مطالب
کوئری نویسی در EF Core - قسمت اول - تشکیل بانک اطلاعاتی و مقدار دهی اولیه‌ی آن
عموم کسانیکه برای بار اول با LINQ آشنا می‌شوند، مشکل ترجمه‌ی کوئری‌های قبلی SQL خود را به آن دارند. به همین جهت پس از چند سعی و خطا ترجیح می‌دهند تا از ORMها استفاده نکنند؛ چون در کوئری نویسی با آن‌ها مشکل دارند. در این سری، تمام مثال‌های سایت PostgreSQL Exercises با EF Core و LINQ to Entities آن پیاده سازی خواهند شد تا بتواند به عنوان راهنمایی برای تازه‌کاران مورد استفاده قرار گیرد.


بررسی ساختار بانک اطلاعاتی تمرین‌های سایت PostgreSQL Exercises

بانک اطلاعاتی مثال‌های سایت PostgreSQL Exercises از سه جدول با مشخصات زیر تشکیل می‌شود:

جدول کاربران
 CREATE TABLE cd.members
    (
       memid integer NOT NULL, 
       surname character varying(200) NOT NULL, 
       firstname character varying(200) NOT NULL, 
       address character varying(300) NOT NULL, 
       zipcode integer NOT NULL, 
       telephone character varying(20) NOT NULL, 
       recommendedby integer,
       joindate timestamp not null,
       CONSTRAINT members_pk PRIMARY KEY (memid),
       CONSTRAINT fk_members_recommendedby FOREIGN KEY (recommendedby)
            REFERENCES cd.members(memid) ON DELETE SET NULL
    );
هر کاربر در اینجا به همراه یک ID و آدرس است. همچنین به همراه اطلاعات کاربری که او را توصیه کرده‌است (یک جدول خود ارجاع دهنده‌است).


جدول امکانات قابل ارائه‌ی به کاربران
   CREATE TABLE cd.facilities
    (
       facid integer NOT NULL, 
       name character varying(100) NOT NULL, 
       membercost numeric NOT NULL, 
       guestcost numeric NOT NULL, 
       initialoutlay numeric NOT NULL, 
       monthlymaintenance numeric NOT NULL, 
       CONSTRAINT facilities_pk PRIMARY KEY (facid)
    );
در این جدول، امکاناتی مانند «زمین تنیس» و امثال آن ثبت می‌شوند؛ به همراه اطلاعاتی مانند هزینه‌ی اجاره‌ی آن توسط کاربران و یا مهمان‌ها که این دو هزینه، با هم متفاوت هستند. همچنین اطلاعاتی مانند هزینه‌ی راه‌اندازی اولیه‌ی آن‌ها، به همراه هزینه‌ی نگهداری ماهیانه‌ی هر کدام از امکانات نیز ثبت می‌شوند؛ تا در آینده بتوان یک سری محاسبات مالی را نیز در مورد امکانات مهیای مجموعه انجام داد تا مشخص شود که آیا برای مثال داشتن مجموعه‌ای خاص، مقرون به صرفه هست یا خیر.


جدول سوابق استفاده‌ی کاربران از امکانات مجموعه
CREATE TABLE cd.bookings
    (
       bookid integer NOT NULL, 
       facid integer NOT NULL, 
       memid integer NOT NULL, 
       starttime timestamp NOT NULL,
       slots integer NOT NULL,
       CONSTRAINT bookings_pk PRIMARY KEY (bookid),
       CONSTRAINT fk_bookings_facid FOREIGN KEY (facid) REFERENCES cd.facilities(facid),
       CONSTRAINT fk_bookings_memid FOREIGN KEY (memid) REFERENCES cd.members(memid)
    );
در این جدول با ثبت ID کاربر و امکاناتی را که درخواست داده، سوابق رزرو آن‌ها نگهداری می‌شوند.
هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعت‌های رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته می‌شود و زمان شروع این رزرو نیز ثبت می‌شود.


تبدیل ساختار بانک اطلاعاتی سایت PostgreSQL Exercises به EF Core Code First


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

موجودیت کاربران

namespace EFCorePgExercises.Entities
{
    public class Member
    {
        public int MemId { set; get; }

        public string Surname { set; get; }

        public string FirstName { set; get; }

        public string Address { set; get; }

        public int ZipCode { set; get; }

        public string Telephone { set; get; }

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

        public DateTime JoinDate { set; get; }

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
خواص این کلاس دقیقا بر اساس فیلدهای جدول کاربران مثال‌های سایت تهیه شده‌است. تنها تفاوت آن، داشتن خواص راهبری (navigation properties) مانند Children، Member و Bookings است که نوع روابط این موجودیت را با سایر موجودیت‌ها مشخص می‌کنند:
- خاصیت‌های Children و Recommender برای تعریف رابطه‌ی «خود ارجاعی» اضافه شده‌اند. در اینجا هر کاربر می‌تواند توسط کاربر دیگری توصیه شده باشد.
- خاصیت Bookings برای بیان رابطه‌ی یک به چند با موجودیت Booking، تعریف شده‌است؛ هر یک کاربر می‌تواند به هر تعدادی رزرو امکانات داشته باشد.


موجودیت Facility

namespace EFCorePgExercises.Entities
{
    public class Facility
    {
        public int FacId { set; get; }

        public string Name { set; get; }

        public decimal MemberCost { set; get; }

        public decimal GuestCost { set; get; }

        public decimal InitialOutlay { set; get; }

        public decimal MonthlyMaintenance { set; get; }

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
- در این جدول، خواص از نوع پولی، توسط نوع decimal معرفی شده‌اند. برای این موارد هیچگاه از double و یا float استفاده نکنید؛ اطلاعات بیشتر.
- خاصیت راهبری Bookings، بیانگر رابطه‌ی یک به چند هرکدام از امکانات مجموعه با تعداد بار و سوابق رزرو شدن آن‌ها است.


موجودیت Booking

namespace EFCorePgExercises.Entities
{
    public class Booking
    {
        public int BookId { set; get; }

        public int FacId { set; get; }
        public virtual Facility Facility { set; get; }

        public int MemId { set; get; }
        public virtual Member Member { set; get; }

        public DateTime StartTime { set; get; }

        public int Slots { set; get; }
    }
}
در جدول ثبت وقایع این مجموعه، اطلاعات کاربر و اطلاعات امکانات درخواستی توسط او ثبت می‌شوند. به همین جهت دو خاصیت راهبری Facility و Member نیز به ازای هر کدام از این Idها تعریف شده‌اند. وجود آن‌ها، جوین نویسی را در آینده بسیار ساده خواهند کرد.


تنظیمات هر کدام از موجودیت‌ها و روابط بین آن‌ها در EF Core Code First

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

تنظیمات موجودیت کاربران

namespace EFCorePgExercises.Entities
{
    public class MemberConfiguration : IEntityTypeConfiguration<Member>
    {
        public void Configure(EntityTypeBuilder<Member> builder)
        {
            builder.HasKey(member => member.MemId);
            builder.Property(member => member.MemId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(member => member.Surname).HasMaxLength(200).IsRequired();
            builder.Property(member => member.FirstName).HasMaxLength(200).IsRequired();
            builder.Property(member => member.Address).HasMaxLength(300).IsRequired();
            builder.Property(member => member.ZipCode).IsRequired();
            builder.Property(member => member.Telephone).HasMaxLength(20).IsRequired();

            builder.HasIndex(member => member.RecommendedBy);
            builder.HasOne(member => member.Recommender)
                    .WithMany(member => member.Children)
                    .HasForeignKey(member => member.RecommendedBy);

            builder.Property(member => member.JoinDate).IsRequired();

            builder.HasIndex(member => member.JoinDate).HasName("IX_JoinDate");
            builder.HasIndex(member => member.RecommendedBy).HasName("IX_RecommendedBy");
        }
    }
}
- در اینجا بر اساس تعاریفی که در ابتدای بحث مشاهده کردید، برای مثال طول هر کدام از فیلدهای رشته‌ای متناظر تعریف شده‌اند.
- سپس نحوه‌ی تعریف رابطه‌ی خود راجاعی این موجودیت را مشاهده می‌کنید.
- دو ایندکس هم در اینجا تعریف شده‌اند که جزو اطلاعات موجود در فایل SQL این سری از مثال‌ها هستند.

نکته‌ی مهم: در اینجا یک UseIdentityColumn(seed: 0, increment: 1) را نیز مشاهده می‌کنید که شاید برای شما تازگی داشته باشد. فیلد ID تمام جداول این مجموعه برخلاف معمول که از 1 شروع می‌شود، از صفر شروع می‌شود و ID مساوی صفر را برای کاربران مهمان درنظر گرفته‌است. روش تعریف چنین تنظیم خاصی را توسط متد UseIdentityColumn و دو پارامتر آن در اینجا مشاهده می‌کنید. این ID مساوی صفر، نکات خاصی را هم در حین ثبت اطلاعات اولیه‌ی هر جدول، به همراه دارد که در ادامه بررسی خواهد شد.


تنظیمات موجودیت امکانات مجموعه

namespace EFCorePgExercises.Entities
{
    public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
    {
        public void Configure(EntityTypeBuilder<Facility> builder)
        {
            builder.HasKey(facility => facility.FacId);
            builder.Property(facility => facility.FacId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(facility => facility.Name).HasMaxLength(100).IsRequired();

            builder.Property(facility => facility.MemberCost).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.GuestCost).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.InitialOutlay).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.MonthlyMaintenance).IsRequired().HasColumnType("decimal(18, 6)");
        }
    }
}
تنها نکته‌ی مهم این تنظیمات، ذکر دقت نوع decimal است؛ بدون تنظیم آن، EF Core در حین اجرای Migrations، اخطاری را صادر می‌کند.


تنظیمات موجودیت سوابق رزرو‌های امکانات مجموعه

namespace EFCorePgExercises.Entities
{
    public class BookingConfiguration : IEntityTypeConfiguration<Booking>
    {
        public void Configure(EntityTypeBuilder<Booking> builder)
        {
            builder.HasKey(booking => booking.BookId);
            builder.Property(booking => booking.BookId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(booking => booking.FacId).IsRequired();
            builder.HasOne(booking => booking.Facility)
                    .WithMany(facility => facility.Bookings)
                    .HasForeignKey(booking => booking.FacId);

            builder.Property(booking => booking.MemId).IsRequired();
            builder.HasOne(booking => booking.Member)
                    .WithMany(member => member.Bookings)
                    .HasForeignKey(booking => booking.MemId);

            builder.Property(booking => booking.StartTime).IsRequired();

            builder.Property(booking => booking.Slots).IsRequired();

            builder.HasIndex(booking => new { booking.MemId, booking.FacId }).HasName("IX_memid_facid");
            builder.HasIndex(booking => new { booking.FacId, booking.StartTime }).HasName("IX_facid_starttime");
            builder.HasIndex(booking => new { booking.MemId, booking.StartTime }).HasName("IX_memid_starttime");
            builder.HasIndex(booking => booking.StartTime).HasName("IX_starttime");
        }
    }
}
روابط یک به چند بین امکانات و رزروها و کاربران و رزروها، در تنظیمات فوق بیان شده‌اند و ذکر آن‌ها در یک سمت رابطه کافی است.


ایجاد Context و معرفی موجودیت‌ها و تنظیمات آن‌ها

در ادامه توسط ApplicationDbContext که از DbContext ارث‌بری می‌کند، سه موجودیت تعریف شده را در معرض دید EF Core قرار می‌دهیم:
namespace EFCorePgExercises.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Member> Members { get; set; }

        public DbSet<Booking> Bookings { get; set; }

        public DbSet<Facility> Facilities { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(MemberConfiguration).Assembly);
        }
    }
}
همچنین تمام تنظیماتی را که تعریف کردیم، توسط یک سطر ApplyConfigurationsFromAssembly می‌توان از اسمبلی دربرگیرنده‌ی آن‌ها خواند و به Context اضافه کرد.


اجرای Migrations جهت تشکیل ساختار بانک اطلاعاتی

اکنون که موجودیت‌ها، روابط بین آن‌ها و Context برنامه مشخص شدند، می‌توان با اجرای دستوارت زیر، سبب تولید کدهای Migration شد که با اجرای آن‌ها، بانک اطلاعاتی متناظری به صورت خودکار تولید می‌شود:
dotnet tool install --global dotnet-ef --version 3.1.6
dotnet tool update --global dotnet-ef --version 3.1.6
dotnet build
dotnet ef migrations add Init --context ApplicationDbContext
در نگارش EF Core 3x، نیاز است ابزار dotnet-ef را به صورت جداگانه‌ای دریافت و یا به روز رسانی کرد (دو دستور اول) و سپس دستور dotnet ef را اجرا نمود.


مقدار دهی اولیه‌ی بانک اطلاعاتی

سایت PostgreSQL Exercises به همراه فایل SQL ایجاد جداول و مقدار دهی اولیه‌ی آن‌ها نیز هست. شاید عنوان کنید که چرا این اطلاعات به صورت متدهای HasData، به تنظیمات موجودیت‌ها اضافه نشدند؟ علت آن به همان ID مساوی صفر بر می‌گردد! در حین استفاده‌ی از متد HasData نمی‌توانید ID ای داشته باشید که مقدار آن با مقدار پیش‌فرض آن نوع، یکی باشد. برای مثال مقدار پیش فرض int، مساوی صفر است. به همین جهت حتی با تنظیم UseIdentityColumn(seed: 0, increment: 1)، اجازه‌ی ثبت Id مساوی صفر را نمی‌دهد؛ چون نمی‌تواند تشخیص دهد که این مقدار، یک مقدار صریح است یا خیر (^). بنابراین مجبور هستیم تا آن‌ها را به صورت معمولی ثبت کنیم:
context.Facilities.Add(new Facility { Name = "Tennis Court 1", MemberCost = 5, GuestCost = 25, InitialOutlay = 10000, MonthlyMaintenance = 200 });
// مابقی موارد
context.SaveChanges();
در این حالت، اول رکورد ثبت شده، Id مساوی صفر را خواهد داشت و مابقی هم یکی یکی افزایش می‌یابند.
این روش برای ثبت اطلاعات Facilities و Booking کار می‌کند؛ اما ... چون Idهای کاربران پشت سر هم نیست و بین آن‌ها فاصله وجود دارد، دیگر نمی‌توان از روش فوق استفاده کرد و نیاز است بتوان مقدار Id را به صورت صریحی تعیین کرد که این مورد نکات جالبی را به همراه دارد:
- در حین کار با SQL Server نیاز است دستور SET IDENTITY_INSERT Members ON را در ابتدای کار، فراخوانی کرد تا بتوان مقدار فیلد ID خود افزایش دهنده را به صورت دستی مقدار دهی کرد.
- در هر زمان، فقط یک جدول و فقط یک سشن (یک اتصال) را می‌توان توسط IDENTITY_INSERT در حالت ثبت و مقدار دهی ID آن قرار داد.
- EF Core، به ازای هر batch اطلاعاتی که ثبت می‌کند، یکبار اتصال را باز و بسته می‌کند. این مورد سبب می‌شود که فراخوانی ExecuteSqlCommand با دستور یاد شده، تاثیری نداشته باشد. برای رفع این مشکل باید یک تراکنش را باز کرد، تا اتصال به بانک اطلاعاتی، در طول آن باز باقی بماند.

در اینجا برای ثبت کاربر با ID مساوی صفر، باز هم می‌توان به صورت معمولی عمل کرد:
context.Members.Add(new Member { ... });
context.SaveChanges(); // For id = 0 = Int's CLR Default Value!
چون اولین رکورد است، ID آن مساوی صفر خواهد شد. برای مابقی از روش ویژه‌ی زیر استفاده می‌کنیم:
using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members ON");

        context.Members.Add(new Member { ... });
        // مابقی موارد

        context.SaveChanges();

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
    finally
    {
        context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members OFF");
    }
}
ابتدا یک تراکنش را بر روی context ایجاد می‌کنیم تا اتصال باز شده، در طول آن ثابت باقی بماند. اکنون اجرای دستور SET IDENTITY_INSERT، مؤثر واقع می‌شود. سپس تمام رکوردها را با ذکر ID صریح آن‌ها به context اضافه کرد، آن‌ها را ذخیره نموده و تراکنش را Commit می‌کنیم. در پایان کار هم باید دستور خاموش کردن SET IDENTITY_INSERT صادر شود.


کدهای کامل موجودیت‌های این قسمت به همراه تنظیمات آن‌ها
کدهای کامل تنظیم Context و همچنین مقدار دهی اولیه‌ی بانک اطلاعاتی
مطالب
EF Code First #1

در ادامه بحث ASP.NET MVC می‌شود به ابزاری به نام MVC Scaffolding اشاره کرد. کار این ابزار که توسط یکی از اعضای تیم ASP.NET MVC به نام استیو اندرسون تهیه شده، تولید کدهای اولیه یک برنامه کامل ASP.NET MVC از روی مدل‌های شما می‌باشد. حجم بالایی از کدهای تکراری آغازین برنامه را می‌شود توسط این ابزار تولید و بعد سفارشی کرد. MVC Scaffolding حتی قابلیت تولید کد بر اساس الگوی Repository و یا نوشتن Unit tests مرتبط را نیز دارد. بدیهی است این ابزار جای یک برنامه نویس را نمی‌تواند پر کند اما کدهای آغازین یک سری کارهای متداول و تکراری را به خوبی می‌تواند پیاده سازی و ارائه دهد. زیر ساخت این ابزار، علاوه بر ASP.NET MVC، آشنایی با Entity framework code first است.
در طی سری ASP.NET MVC که در این سایت تا به اینجا مطالعه کردید من به شدت سعی کردم از ابزارگرایی پرهیز کنم. چون شخصی که نمی‌داند مسیریابی چیست، اطلاعات چگونه به یک کنترلر منتقل یا به یک View ارسال می‌شوند، قراردادهای پیش فرض فریم ورک چیست یا زیر ساخت امنیتی یا فیلترهای ASP.NET MVC کدامند، چطور می‌تواند از ابزار پیشرفته Code generator ایی استفاده کند، یا حتی در ادامه کدهای تولیدی آن‌را سفارشی سازی کند؟ بنابراین برای استفاده از این ابزار و درک کدهای تولیدی آن، نیاز به یک پیشنیاز دیگر هم وجود دارد: «Entity framework code first»
امسال دو کتاب خوب در این زمینه منتشر شده‌اند به نام‌های:
Programming Entity Framework: DbContext, ISBN: 978-1-449-31296-1
Programming Entity Framework: Code First, ISBN: 978-1-449-31294-7
که هر دو به صورت اختصاصی به مقوله EF Code first پرداخته‌اند.


در طی روزهای بعدی EF Code first را با هم مرور خواهیم کرد و البته این مرور مستقل است از نوع فناوری میزبان آن؛ می‌خواهد یک برنامه کنسول باشد یا WPF یا یک سرویس ویندوز NT و یا ... یک برنامه وب.
البته از دیدگاه مایکروسافت، M در MVC به معنای EF Code first است. به همین جهت MVC3 به صورت پیش فرض ارجاعی را به اسمبلی‌های آن دارد و یا حتی به روز رسانی که برای آن ارائه داده نیز در جهت تکمیل همین بحث است.


مروری سریع بر تاریخچه Entity framework code first

ویژوال استودیو 2010 و دات نت 4، به همراه EF 4.0 ارائه شدند. با این نگارش امکان استفاده از حالت‌های طراحی database first و model first مهیا است. پس از آن، به روز رسانی‌های EF خارج از نوبت و به صورت منظم، هر از چندگاهی ارائه می‌شوند و در زمان نگارش این مطلب، آخرین نگارش پایدار در دسترس آن 4.3.1 می‌باشد. از زمان EF 4.1 به بعد، نوع جدیدی از مدل سازی به نام Code first به این فریم ورک اضافه شد و در نگارش‌های بعدی آن، مباحث DB migration جهت ساده سازی تطابق اطلاعات مدل‌ها با بانک اطلاعاتی، اضافه گردیدند. در روش Code first، کار با طراحی کلاس‌ها که در اینجا مدل داده‌ها نامیده می‌شوند، شروع گردیده و سپس بر اساس این اطلاعات، تولید یک بانک اطلاعاتی جدید و یا استفاده از نمونه‌ای موجود میسر می‌گردد.
پیشتر در روش database first ابتدا یک بانک اطلاعاتی موجود، مهندسی معکوس می‌شد و از روی آن فایل XML ایی با پسوند EDMX تولید می‌گشت. سپس به کمک entity data model designer ویژوال استودیو، این فایل نمایش داده شده و یا امکان اعمال تغییرات بر روی آن میسر می‌شد. همچنین در روش دیگری به نام model first نیز کار از entity data model designer جهت طراحی موجودیت‌ها آغاز می‌گشت.
اما با روش Code first دیگر در ابتدای امر مدل فیزیکی و یک بانک اطلاعاتی وجود خارجی ندارد. در اینجا EF تعاریف کلاس‌های شما را بررسی کرده و بر اساس آن، اطلاعات نگاشت‌های خواص کلاس‌ها به جداول و فیلدهای بانک اطلاعاتی را تشکیل می‌دهد. البته عموما تعاریف ساده کلاس‌ها بر این منظور کافی نیستند. به همین جهت از یک سری متادیتا به نام ویژگی‌ها یا اصطلاحا data annotations مهیا در فضای نام System.ComponentModel.DataAnnotations برای افزودن اطلاعات لازم مانند نام فیلدها، جداول و یا تعاریف روابط ویژه نیز استفاده می‌گردد. به علاوه در روش Code first یک API جدید به نام Fluent API نیز جهت تعاریف این ویژگی‌ها و روابط، با کدنویسی مستقیم نیز درنظر گرفته شده است. نهایتا از این اطلاعات جهت نگاشت کلاس‌ها به بانک اطلاعاتی و یا برای تولید ساختار یک بانک اطلاعاتی خالی جدید نیز می‌توان کمک گرفت.



مزایای EF Code first

- مطلوب برنامه نویس‌ها! : برنامه نویس‌هایی که مدتی تجربه کار با ابزارهای طراح را داشته باشند به خوبی می‌دانند این نوع ابزارها عموما demo-ware هستند. چندجا کلیک می‌کنید، دوبار Next، سه بار OK و ... به نظر می‌رسد کار تمام شده. اما واقعیت این است که عمری را باید صرف نگهداری و یا پیاده سازی جزئیاتی کرد که انجام آن‌ها با کدنویسی مستقیم بسیار سریعتر، ساده‌تر و با کنترل بیشتری قابل انجام است.
- سرعت: برای کار با EF Code first نیازی نیست در ابتدای کار بانک اطلاعاتی خاصی وجود داشته باشد. کلا‌س‌های خود را طراحی و شروع به کدنویسی کنید.
- سادگی: در اینجا دیگر از فایل‌های EDMX خبری نیست و نیازی نیست مرتبا آن‌ها را به روز کرده یا نگهداری کرد. تمام کارها را با کدنویسی و کنترل بیشتری می‌توان انجام داد. به علاوه کنترل کاملی بر روی کد نهایی تهیه شده نیز وجود دارد و توسط ابزارهای تولید کد، ایجاد نمی‌شوند.
- طراحی بهتر بانک اطلاعاتی نهایی: اگر طرح دقیقی از مدل‌های برنامه داشته باشیم، می‌توان آن‌ها را به المان‌های کوچک و مشخصی، تقسیم و refactor کرد. همین مساله در نهایت مباحث database normalization را به نحوی مطلوب و با سرعت بیشتری میسر می‌کند.
- امکان استفاده مجدد از طراحی کلاس‌های انجام شده در سایر ORMهای دیگر. چون طراحی مدل‌های برنامه به بانک اطلاعاتی خاصی گره نمی‌خورند و همچنین الزاما هم قرار نیست جزئیات کاری EF در آن‌ها لحاظ شود، این کلاس‌ها در صورت نیاز در سایر پروژه‌ها نیز به سادگی قابل استفاده هستند.
- ردیابی ساده‌تر تغییرات: روش اصولی کار با پروژه‌های نرم افزاری همواره شامل استفاده از یک ابزار سورس کنترل مانند SVN، Git، مرکوریال و امثال آن است. به این ترتیب ردیابی تغییرات انجام شده به سادگی توسط این ابزارها میسر می‌شوند.
- ساده‌تر شدن طراحی‌های پیچیده‌تر: برای مثال پیاده سازی ارث بری،‌ ایجاد کلاس‌های خود ارجاع دهنده و امثال آن با کدنویسی ساده‌تر است.


دریافت آخرین نگارش EF


برای دریافت و نصب آخرین نگارش EF نیاز است از NuGet استفاده شود و این مزایا را به همراه دارد:
به کمک NuGet امکان با خبر شدن از به روز رسانی جدید صورت گرفته به صورت خودکار درنظر گرفته شده است و همچنین کار دریافت بسته‌های مرتبط و به روز رسانی ارجاعات نیز در این حالت خودکار است. به علاوه توسط NuGet امکان دسترسی به کتابخانه‌هایی که مثلا در گوگل‌کد قرار دارند و به صورت معمول امکان دریافت آن‌ها برای ما میسر نیست، نیز بدون مشکل فراهم است (برای نمونه ELMAH، که اصل آن از گوگل‌کد قابل دریافت است؛ اما بسته نیوگت آن نیز در دسترس می‌باشد).
پس از نصب NuGet، تنها کافی است بر روی گره References در Solution explorer ویژوال استودیو، کلیک راست کرده و به کمک NuGet آخرین نگارش EF را نصب کرد. در گالری آنلاین آن، عموما EF اولین گزینه است (به علت تعداد بالای دریافت آن).
حین استفاده از NuGet جهت نصب Ef، ابتدا ارجاعاتی به اسمبلی‌های زیر به برنامه اضافه خواهند شد:
System.ComponentModel.DataAnnotations.dll
System.Data.Entity.dll
EntityFramework.dll
بدیهی است بدون استفاده از NuGet، تمام این کارها را باید دستی انجام داد.
سپس در پوشه‌ای به نام packages، فایل‌های مرتبط با EF قرار خواهند گرفت که شامل اسمبلی آن به همراه ابزارهای DB Migration است. همچنین فایل packages.config که شامل تعاریف اسمبلی‌های نصب شده است به پروژه اضافه می‌شود. NuGet به کمک این فایل و شماره نگارش درج شده در آن، شما را از به روز رسانی‌های بعدی مطلع خواهد ساخت:

<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="4.3.1" />
</packages>

همچنین اگر به فایل app.config یا web.config برنامه نیز مراجعه کنید، یک سری تنظیمات ابتدایی اتصال به بانک اطلاعاتی در آن ذکر شده است:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
<parameters>
<parameter value="Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
</configuration>

همانطور که ملاحظه می‌کنید بانک اطلاعاتی پیش فرضی که در اینجا ذکر شده است، LocalDB می‌باشد. این بانک اطلاعاتی را از این آدرس‌ نیز می‌توانید دریافت کنید.

البته ضرورتی هم به استفاده از آن نیست و از سایر نگارش‌های SQL Server نیز می‌توان استفاده کرد ولی خوب ... مزیت استفاده از آن برای کاربر نهایی این است که «نیازی به یک مهندس برای نصب، راه اندازی و نگهداری ندارد». تنها مشکل آن این است که از ویندوز XP پشتیبانی نمی‌کند. البته SQL Server CE 4.0 این محدودیت را ندارد.
ضمن اینکه باید درنظر داشت EF به فناوری میزبان خاصی گره نخورده است و مثال‌هایی که در اینجا بررسی می‌شوند صرفا تعدادی برنامه کنسول معمولی هستند و نکات عنوان شده در آن‌ها در تمام فناوری‌های میزبان موجود به یک نحو کاربرد دارند.


قراردادهای پیش فرض EF Code first

عنوان شد که اطلاعات کلاس‌های ساده تشکیل دهنده مدل‌های برنامه، برای تعریف جداول و فیلدهای یک بانک اطلاعات و همچنین مشخص سازی روابط بین آن‌ها کافی نیستند و مرسوم است برای پر کردن این خلاء از یک سری متادیتا و یا Fluent API مهیا نیز استفاده گردد. اما در EF Code first یک سری قرار داد توکار نیز وجود دارند که مطلع بودن از آن‌ها سبب خواهد شد تا حجم کدنویسی و تنظیمات جانبی این فریم ورک به حداقل برسند. برای نمونه مدل‌های معروف بلاگ و مطالب آن‌را درنظر بگیرید:

namespace EF_Sample01.Models
{
public class Post
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public virtual Blog Blog { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample01.Models
{
public class Blog
{
public int Id { set; get; }
public string Title { set; get; }
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}


یکی از قراردادهای EF Code first این است که کلاس‌های مدل شما را جهت یافتن خاصیتی به نام Id یا ClassId مانند BlogId، جستجو می‌کند و از آن به عنوان primary key و فیلد identity جدول بانک اطلاعاتی استفاده خواهد کرد.
همچنین در کلاس Blog، خاصیت لیستی از Posts و در کلاس Post خاصیت virtual ایی به نام Blog وجود دارند. به این ترتیب روابط بین دو کلاس و ایجاد کلید خارجی متناظر با آن‌را به صورت خودکار انجام خواهد داد.
نهایتا از این اطلاعات جهت تشکیل database schema یا ساختار بانک اطلاعاتی استفاده می‌گردد.
اگر به فضاهای نام دو کلاس فوق دقت کرده باشید، به کلمه Models ختم شده‌اند. به این معنا که در پوشه‌ای به همین نام در پروژه جاری قرار دارند. یا مرسوم است کلاس‌های مدل را در یک پروژه class library مجزا به نام DomainClasses نیز قرار دهند. این پروژه نیازی به ارجاعات اسمبلی‌های EF ندارد و تنها به اسمبلی System.ComponentModel.DataAnnotations.dll نیاز خواهد داشت.


EF Code first چگونه کلاس‌های مورد نظر را انتخاب می‌کند؟

ممکن است ده‌ها و صدها کلاس در یک پروژه وجود داشته باشند. EF Code first چگونه از بین این کلاس‌ها تشخیص خواهد داد که باید از کدامیک استفاده کند؟ اینجا است که مفهوم جدیدی به نام DbContext معرفی شده است. برای تعریف آن یک کلاس دیگر را به پروژه برای مثال به نام Context اضافه کنید. همچنین مرسوم است که این کلاس را در پروژه class library دیگری به نام DataLayer اضافه می‌کنند. این پروژه نیاز به ارجاعی به اسمبلی‌های EF خواهد داشت. در ادامه کلاس جدید اضافه شده باید از کلاس DbContext مشتق شود:

using System.Data.Entity;
using EF_Sample01.Models;

namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
}
}

سپس در اینجا به کمک نوع جنریکی به نام DbSet، کلاس‌های دومین برنامه را معرفی می‌کنیم. به این ترتیب، EF Code first ابتدا به دنبال کلاسی مشتق شده از DbContext خواهد گشت. پس از یافتن آن‌، خواصی از نوع DbSet را بررسی کرده و نوع‌های متناظر با آن‌را به عنوان کلاس‌های دومین درنظر می‌گیرد و از سایر کلاس‌های برنامه صرفنظر خواهد کرد. این کل کاری است که باید انجام شود.
اگر دقت کرده باشید، نام کلاس‌های موجودیت‌ها، مفرد هستند و نام خواص تعریف شده به کمک DbSet‌، جمع می‌باشند که نهایتا متناظر خواهند بود با نام جداول بانک اطلاعاتی تشکیل شده.


تشکیل خودکار بانک اطلاعاتی و افزودن اطلاعات به جداول

تا اینجا بدون تهیه یک بانک اطلاعاتی نیز می‌توان از کلاس Context تهیه شده استفاده کرد و کار کدنویسی را آغاز نمود. بدیهی است جهت اجرای نهایی کدها، نیاز به یک بانک اطلاعاتی خواهد بود. اگر تنظیمات پیش فرض فایل کانفیگ برنامه را تغییر ندهیم، از همان defaultConnectionFactory یاده شده استفاده خواهد کرد. در این حالت نام بانک اطلاعاتی به صورت خودکار تنظیم شده و مساوی «EF_Sample01.Context» خواهد بود.
برای سفارشی سازی آن نیاز است فایل app.config یا web.config برنامه را اندکی ویرایش نمود:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
...
</configSections>
<connectionStrings>
<clear/>
<add name="Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
...
</configuration>

در اینجا به بانک اطلاعاتی testdb2012 در وهله پیش فرض SQL Server نصب شده، اشاره شده است. فقط باید دقت داشت که تگ configSections باید در ابتدای فایل قرار گیرد و مابقی تنظیمات پس از آن.
یا اگر علاقمند باشید که از SQL Server CE استفاده کنید، تنظیمات رشته اتصالی را به نحو زیر مقدار دهی نمائید:

<connectionStrings> 
              <add name="MyContextName"
                         connectionString="Data Source=|DataDirectory|\Store.sdf"
                         providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>

در هر دو حالت، name باید به نام کلاس مشتق شده از DbContext اشاره کند که در مثال جاری همان Context است.
یا اگر علاقمند بودید که این قرارداد توکار را تغییر داده و نام رشته اتصالی را با کدنویسی تعیین کنید، می‌توان به نحو زیر عمل کرد:

public class Context : DbContext
{
    public Context()
      : base("ConnectionStringName")
    {
    }


البته ضرورتی ندارد این بانک اطلاعاتی از پیش موجود باشد. در اولین بار اجرای کدهای زیر، به صورت خودکار بانک اطلاعاتی و جداول Blogs و Posts و روابط بین آن‌ها تشکیل می‌گردد:

using EF_Sample01.Models;

namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}


در این تصویر چند نکته حائز اهمیت هستند:
الف) نام پیش فرض بانک اطلاعاتی که به آن اشاره شد (اگر تنظیمات رشته اتصالی قید نگردد).
ب) تشکیل خودکار primary key از روی خواصی به نام Id
ج) تشکیل خودکار روابط بین جداول و ایجاد کلید خارجی (به کمک خاصیت virtual تعریف شده)
د) تشکیل جدول سیستمی به نام dbo.__MigrationHistory که از آن برای نگهداری سابقه به روز رسانی‌های ساختار جداول کمک گرفته خواهد شد.
ه) نوع و طول فیلدهای متنی، nvarchar از نوع max است.

تمام این‌ها بر اساس پیش فرض‌ها و قراردادهای توکار EF Code first انجام شده است.

در کدهای تعریف شده نیز، ابتدا یک وهله از شیء Context ایجاد شده و سپس به کمک آن می‌توان به جدول Blogs اطلاعاتی را افزود و در آخر ذخیره نمود. استفاده از using هم دراینجا نباید فراموش شود، زیرا اگر استثنایی در این بین رخ دهد، کار پاکسازی منابع و بستن اتصال گشوده شده به بانک اطلاعاتی به صورت خودکار انجام خواهد شد.
در ادامه اگر بخواهیم مطلبی را به Blog ثبت شده اضافه کنیم، خواهیم داشت:

using EF_Sample01.Models;

namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
//addBlog();
addPost();
}

private static void addPost()
{
using (var db = new Context())
{
var blog = db.Blogs.Find(1);
db.Posts.Add(new Post
{
Blog = blog,
Content = "data",
Title = "EF"
});
db.SaveChanges();
}
}

private static void addBlog()
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}

متد db.Blogs.Find، بر اساس primary key بلاگ ثبت شده، یک وهله از آن‌را یافته و سپس از آن جهت تشکیل شیء Post و افزودن آن به جدول Posts استفاده می‌شود. متد Find ابتدا Contxet جاری را جهت یافتن شیءایی با id مساوی یک جستجو می‌کند (اصطلاحا به آن first level cache هم گفته می‌شود). اگر موفق به یافتن آن شد، بدون صدور کوئری اضافه‌ای به بانک اطلاعاتی از اطلاعات همان شیء استفاده خواهد کرد. در غیراینصورت نیاز خواهد داشت تا ابتدا کوئری لازم را به بانک اطلاعاتی ارسال کرده و اطلاعات شیء Blog متناظر با id=1 را دریافت کند. همچنین اگر نیاز داشتیم تا تنها با سطح اول کش کار کنیم، در EF Code first می‌توان از خاصیتی به نام Local نیز استفاده کرد. برای مثال خاصیت db.Blogs.Local بیانگر اطلاعات موجود در سطح اول کش می‌باشد.
نهایتا کوئری Insert تولید شده توسط آن به شکل زیر خواهد بود (لاگ شده توسط برنامه SQL Server Profiler):

exec sp_executesql N'insert [dbo].[Posts]([Title], [Content], [Blog_Id])
values (@0, @1, @2)
select [Id]
from [dbo].[Posts]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',
N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',
@0=N'EF',
@1=N'data',
@2=1


این نوع کوئرهای پارامتری چندین مزیت مهم را به همراه دارند:
الف) به صورت خودکار تشکیل می‌شوند. تمام کوئری‌های پشت صحنه EF پارامتری هستند و نیازی نیست مرتبا مزایای این امر را گوشزد کرد و باز هم عده‌ای با جمع زدن رشته‌ها نسبت به نوشتن کوئری‌های نا امن SQL اقدام کنند.
ب) کوئرهای پارامتری در مقابل حملات تزریق اس کیوال مقاوم هستند.
ج) SQL Server با کوئری‌های پارامتری همانند رویه‌های ذخیره شده رفتار می‌کند. یعنی query execution plan محاسبه شده آن‌ها را کش خواهد کرد. همین امر سبب بالا رفتن کارآیی برنامه در فراخوانی‌های بعدی می‌گردد. الگوی کلی مشخص است. فقط پارامترهای آن تغییر می‌کنند.
د) مصرف حافظه SQL Server کاهش می‌یابد. چون SQL Server مجبور نیست به ازای هر کوئری اصطلاحا Ad Hoc رسیده یکبار execution plan متفاوت آن‌ها را محاسبه و سپس کش کند. این مورد مشکل مهم تمام برنامه‌هایی است که از کوئری‌های پارامتری استفاده نمی‌کنند؛ تا حدی که گاهی تصور می‌کنند شاید SQL Server دچار نشتی حافظه شده، اما مشکل جای دیگری است.


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

تا همینجا با حداقل کدنویسی و تنظیمات مرتبط با آن، پیشرفت خوبی داشته‌ایم؛ اما نتیجه حاصل آنچنان مطلوب نیست و نیاز به سفارشی سازی دارد. برای مثال طول فیلدها را نیاز داریم به مقدار دیگری تنظیم کنیم، تعدادی از فیلدها باید به صورت not null تعریف شوند یا نام پیش فرض بانک اطلاعاتی باید مشخص گردد و مواردی از این دست. با این موارد در قسمت‌های بعدی بیشتر آشنا خواهیم شد.
نظرات مطالب
بررسی مقدمات کتابخانه‌ی JSON.NET
یک نکته‌ی تکمیلی
استفاده از استریم‌ها برای کار با فایل‌ها در JSON.NET
        public static T DeserializeFromFile<T>(string filePath, JsonSerializerSettings settings = null)
        {
            if (!File.Exists(filePath))
                return default(T);

            using (var fileStream = File.OpenRead(filePath))
            {
                using (var streamReader = new StreamReader(fileStream))
                {
                    using (var reader = new JsonTextReader(streamReader))
                    {
                        var serializer = settings == null ? JsonSerializer.Create() : JsonSerializer.Create(settings);
                        return serializer.Deserialize<T>(reader);
                    }
                }
            }
        }

        public static void SerializeToFile(string filePath, object data, JsonSerializerSettings settings = null)
        {
            using (var fileStream = new FileStream(filePath, FileMode.Create))
            {
                using (var streamReader = new StreamWriter(fileStream))
                {
                    using (var reader = new JsonTextWriter(streamReader))
                    {
                        var serializer = settings == null ? JsonSerializer.Create() : JsonSerializer.Create(settings);
                        serializer.Serialize(reader, data);
                    }
                }
            }
        }
نظرات مطالب
پشتیبانی از حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0

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

در همین مثال مطلب جاری، فرض کنید موجودیت کاربر، به همراه خاصیت محاسباتی تعداد کتاب‌ها (NumberOfBooks) هم هست:

public class User
{
    // ...

    public virtual List<Book> Books { get; set; } = new();

    public int NumberOfBooks { get; set; }
}

اگر سعی کنیم این خاصیت را به صورت زیر به‌روز رسانی کنیم که در آن مقدار تعداد کتاب‌ها به کمک خاصیت راهبری Books محاسبه شود:

await context.Users.Where(x=> x.Id == 1).ExecuteUpdateAsync(s => s.SetProperty(user => user.NumberOfBooks,
                user => user.Books.Count()));

به خطای زیر می‌رسیم:

... could not be translated. Additional information: Translation of member 'Books' on entity type 'User' failed. This commonly occurs when the specified member is unmapped.

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

await context.Users
             .Where(user => user.Id == 1)
             .Select(user => new
             {
                user.NumberOfBooks,
                BooksCount = user.Books.Count()
             })
             .ExecuteUpdateAsync(s => s.SetProperty(arg => arg.NumberOfBooks, arg => arg.BooksCount));

در اینجا تنها کاری که انجام شده، انجام محاسبات و نگاشت‌های لازم، پیش از فراخوانی متد ExecuteUpdateAsync است.

نظرات مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
باید از Dictionary استفاده کنید برای تعریف خواص پویا:
public class Field
{
  [JsonExtensionData]
  public Dictionary<string, object> Property { get; set; }
}
 
public class FieldType
{
  public string Type { get; set; }
}
و بعد نحوه استفاده از آن به صورت زیر خواهد بود:
var data = new
{
  model = new
  {
   fields = new List<Field>
   {
    new Field
    {
      Property = new Dictionary<string, object>
      {
       {"Id", new FieldType { Type = "number" }},
       {"Name", new FieldType { Type = "string" }}
      }
    }
   }
  }
};
var dataJson = JsonConvert.SerializeObject(data, Formatting.Indented);
با این خروجی:
{
  "model": {
    "fields": [
      {
        "Id": {
          "Type": "number"
        },
        "Name": {
          "Type": "string"
        }
      }
    ]
  }
}
- اگر از Web API استفاده می‌کنید، ذکر سطر JsonConvert.SerializeObject ضروری نیست و به صورت توکار از JSON.NET‌ استفاده می‌کند.
- اگر از ASP.NET MVC استفاده می‌کنید، نیاز است از آن کمک بگیرید. از این جهت که خاصیت JsonExtensionData سبب می‌شود تا نام ثابت خاصیت Property، از خروجی نهایی حذف شود و اعضای دیکشنری، جزئی از خاصیت‌های موجود شوند.
- نکته‌ی «گرفتن خروجی CamelCase از JSON.NET» را هم باید مد نظر داشته باشید.
مطالب
پیاده سازی Password Policy سفارشی توسط ASP.NET Identity
برای فراهم کردن یک تجربه کاربری ایمن‌تر و بهتر، ممکن است بخواهید پیچیدگی password policy را سفارشی سازی کنید. مثلا ممکن است بخواهید حداقل تعداد کاراکتر‌ها را تنظیم کنید، استفاده از چند حروف ویژه را اجباری کنید،  جلوگیری از استفاده نام کاربر در کلمه عبور و غیره. برای اطلاعات بیشتر درباره سیاست‌های کلمه عبور به این لینک مراجعه کنید. بصورت پیش فرض ASP.NET Identity کاربران را وادار می‌کند تا کلمه‌های عبوری بطول حداقل 6 کاراکتر وارد نمایند. در ادامه نحوه افزودن چند خط مشی دیگر را هم بررسی می‌کنیم.

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

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


ایجاد اپلیکیشن جدید

در Visual Studio 2013 اپلیکیشن جدیدی از نوع ASP.NET MVC 4.5 بسازید.

در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. به قسمت Update بروید و تمام انتشارات جدید را در صورت وجود نصب کنید.

بگذارید تا به روند کلی ایجاد کاربران جدید در اپلیکیشن نگاهی بیاندازیم. این به ما در شناسایی نیازهای جدیدمان کمک می‌کند. در پوشه Controllers فایلی بنام AccountController.cs وجود دارد که حاوی متدهایی برای مدیریت کاربران است.

  • کنترلر Account از کلاس UserManager استفاده می‌کند که در فریم ورک Identity تعریف شده است. این کلاس به نوبه خود از کلاس دیگری بنام UserStore استفاده می‌کند که برای دسترسی و مدیریت داده‌های کاربران استفاده می‌شود. در مثال ما این کلاس از Entity Framework استفاده می‌کند که پیاده سازی پیش فرض است.
  • متد Register POST یک کاربر جدید می‌سازد. متد CreateAsync به طبع متد 'ValidateAsync' را روی خاصیت PasswordValidator فراخوانی می‌کند تا کلمه عبور دریافتی اعتبارسنجی شود.
var user = new ApplicationUser() { UserName = model.UserName };  
var result = await UserManager.CreateAsync(user, model.Password);
  
if (result.Succeeded)
{
    await SignInAsync(user, isPersistent: false);  
    return RedirectToAction("Index", "Home");
}

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


قانون 1: کلمه‌های عبور باید حداقل 10 کاراکتر باشند

بصورت پیش فرض خاصیت PasswordValidator در کلاس UserManager به کلاس MinimumLengthValidator تنظیم شده است، که اطمینان حاصل می‌کند کلمه عبور حداقل 6 کاراکتر باشد. هنگام وهله سازی UserManager می‌توانید این مقدار را تغییر دهید.
  • مقدار حداقل کاراکترهای کلمه عبور به دو شکل می‌تواند تعریف شود. راه اول، تغییر کنترلر Account است. در متد سازنده این کنترلر کلاس UserManager وهله سازی می‌شود، همینجا می‌توانید این تغییر را اعمال کنید. راه دوم، ساختن کلاس جدیدی است که از UserManager ارث بری می‌کند. سپس می‌توان این کلاس را در سطح global تعریف کرد. در پوشه IdentityExtensions کلاس جدیدی با نام ApplicationUserManager بسازید.
public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(): base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
    {
        PasswordValidator = new MinimumLengthValidator (10);
    }
}

  کلاس UserManager یک نمونه از کلاس IUserStore را دریافت می‌کند که پیاده سازی API‌های مدیریت کاربران است. از آنجا که کلاس UserStore مبتنی بر Entity Framework است، باید آبجکت DbContext را هم پاس دهیم. این کد در واقع همان کدی است که در متد سازنده کنترلر Account وجود دارد.

یک مزیت دیگر این روش این است که می‌توانیم متدهای UserManager را بازنویسی (overwrite) کنیم. برای پیاده سازی نیازمندهای بعدی دقیقا همین کار را خواهیم کرد.


  • حال باید کلاس ApplicationUserManager را در کنترلر Account استفاده کنیم. متد سازنده و خاصیت UserManager را مانند زیر تغییر دهید.
 public AccountController() : this(new ApplicationUserManager())
         {
         }
  
         public AccountController(ApplicationUserManager userManager)
         {
             UserManager = userManager;
         }
         public ApplicationUserManager UserManager { get; private set; }
حالا داریم از کلاس سفارشی جدیدمان استفاده می‌کنیم. این به ما اجازه می‌دهد مراحل بعدی سفارشی سازی را انجام دهیم، بدون آنکه کدهای موجود در کنترلر از کار بیافتند.
  • اپلیکیشن را اجرا کنید و سعی کنید کاربر محلی جدیدی ثبت نمایید. اگر کلمه عبور وارد شده کمتر از 10 کاراکتر باشد پیغام خطای زیر را دریافت می‌کنید.


قانون 2: کلمه‌های عبور باید حداقل یک عدد و یک کاراکتر ویژه داشته باشند

چیزی که در این مرحله نیاز داریم کلاس جدیدی است که اینترفیس IIdentityValidator را پیاده سازی می‌کند. چیزی که ما می‌خواهیم اعتبارسنجی کنیم، وجود اعداد و کاراکترهای ویژه در کلمه عبور است، همچنین طول مجاز هم بررسی می‌شود. نهایتا این قوانین اعتبارسنجی در متد 'ValidateAsync' بکار گرفته خواهند شد.
  • در پوشه IdentityExtensions کلاس جدیدی بنام CustomPasswordValidator بسازید و اینترفیس مذکور را پیاده سازی کنید. از آنجا که نوع کلمه عبور رشته (string) است از <IIdentityValidator<string استفاده می‌کنیم.
public class CustomPasswordValidator : IIdentityValidator<string> 
{  
    public int RequiredLength { get; set; }

    public CustomPasswordValidator(int length)
    {
        RequiredLength = length;
    }
  
    public Task<IdentityResult> ValidateAsync(string item)
    {
        if (String.IsNullOrEmpty(item) || item.Length < RequiredLength)
        {
            return Task.FromResult(IdentityResult.Failed(String.Format("Password should be of length {0}",RequiredLength)));
        }
  
    string pattern = @"^(?=.*[0-9])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*0-9]{10,}$";
   
    if (!Regex.IsMatch(item, pattern))  
    {
        return Task.FromResult(IdentityResult.Failed("Password should have one numeral and one special character"));
    }
  
    return Task.FromResult(IdentityResult.Success);
 }


  در متد ValidateAsync بررس می‌کنیم که طول کلمه عبور معتبر و مجاز است یا خیر. سپس با استفاده از یک RegEx وجود کاراکترهای ویژه و اعداد را بررسی می‌کنیم. دقت کنید که regex استفاده شده تست نشده و تنها بعنوان یک مثال باید در نظر گرفته شود.
  • قدم بعدی تعریف این اعتبارسنج سفارشی در کلاس UserManager است. باید مقدار خاصیت PasswordValidator را به این کلاس تنظیم کنیم. به کلاس ApplicationUserManager که پیشتر ساختید بروید و مقدار خاصیت PasswordValidator را به CustomPasswordValidator تغییر دهید.
public class ApplicationUserManager : UserManager<ApplicationUser>  
{  
    public ApplicationUserManager() : base(new UserStore<ApplicationUser(new ApplicationDbContext()))
    {
        PasswordValidator = new CustomPasswordValidator(10);
    }
 }

  هیچ تغییر دیگری در کلاس AccountController لازم نیست. حال سعی کنید کاربر جدید دیگری بسازید، اما اینبار کلمه عبوری وارد کنید که خطای اعتبارسنجی تولید کند. پیغام خطایی مشابه تصویر زیر باید دریافت کنید.


قانون 3: امکان استفاده از 5 کلمه عبور اخیر ثبت شده وجود ندارد

هنگامی که کاربران سیستم، کلمه عبور خود را بازنشانی (reset) می‌کنند یا تغییر می‌دهند، می‌توانیم بررسی کنیم که آیا مجددا از یک کلمه عبور پیشین استفاده کرده اند یا خیر. این بررسی بصورت پیش فرض انجام نمی‌شود، چرا که سیستم Identity تاریخچه کلمه‌های عبور کاربران را ذخیره نمی‌کند. می‌توانیم در اپلیکیشن خود جدول جدیدی بسازیم و تاریخچه کلمات عبور کاربران را در آن ذخیره کنیم. هربار که کاربر سعی در بازنشانی یا تغییر کلمه عبور خود دارد، مقدار Hash شده را در جدول تاریخچه بررسی میکنیم.
فایل IdentityModels.cs را باز کنید. مانند لیست زیر، کلاس جدیدی بنام 'PreviousPassword' بسازید.
public class PreviousPassword  
{
  
 public PreviousPassword()
 {
    CreateDate = DateTimeOffset.Now;
 }
  
 [Key, Column(Order = 0)]
  
 public string PasswordHash { get; set; }
  
 public DateTimeOffset CreateDate { get; set; }
  
 [Key, Column(Order = 1)]
  
 public string UserId { get; set; }
  
 public virtual ApplicationUser User { get; set; }
  
 }
در این کلاس، فیلد 'Password' مقدار Hash شده کلمه عبور را نگاه میدارد و توسط فیلد 'UserId' رفرنس می‌شود. فیلد 'CreateDate' یک مقدار timestamp ذخیره می‌کند که تاریخ ثبت کلمه عبور را مشخص می‌نماید. توسط این فیلد می‌توانیم تاریخچه کلمات عبور را فیلتر کنیم و مثلا 5 رکورد آخر را بگیریم.

Entity Framework Code First جدول 'PreviousPasswords' را می‌سازد و با استفاده از فیلدهای 'UserId' و 'Password' کلید اصلی (composite primary key) را ایجاد می‌کند. برای اطلاعات بیشتر درباره قرارداهای EF Code First به  این لینک  مراجعه کنید.

  • خاصیت جدیدی به کلاس ApplicationUser اضافه کنید تا لیست آخرین کلمات عبور استفاده شده را نگهداری کند.
public class ApplicationUser : IdentityUser
{
    public ApplicationUser() : base()
    {
        PreviousUserPasswords = new List<PreviousPassword>();
     }

     public virtual IList<PreviousPassword> PreviousUserPasswords { get; set; }
}

 همانطور که پیشتر گفته شد، کلاس UserStore پیاده سازی API‌های لازم برای مدیریت کاربران را در بر می‌گیرد. هنگامی که کاربر برای نخستین بار در سایت ثبت می‌شود باید مقدار Hash کلمه عبورش را در جدول تاریخچه کلمات عبور ذخیره کنیم. از آنجا که UserStore بصورت پیش فرض متدی برای چنین عملیاتی معرفی نمی‌کند، باید یک override تعریف کنیم تا این مراحل را انجام دهیم. پس ابتدا باید کلاس سفارشی جدیدی بسازیم که از UserStore ارث بری کرده و آن را توسعه می‌دهد. سپس از این کلاس سفارشی در ApplicationUserManager بعنوان پیاده سازی پیش فرض UserStore استفاده می‌کنیم. پس کلاس جدیدی در پوشه IdentityExtensions ایجاد کنید.
public class ApplicationUserStore : UserStore<ApplicationUser>
{ 
    public ApplicationUserStore(DbContext context) : base(context)  { }
  
    public override async Task CreateAsync(ApplicationUser user)
    {
        await base.CreateAsync(user);
        await AddToPreviousPasswordsAsync(user, user.PasswordHash);
    }
  
     public Task AddToPreviousPasswordsAsync(ApplicationUser user, string password)
     {
        user.PreviousUserPasswords.Add(new PreviousPassword() { UserId = user.Id, PasswordHash = password });
  
        return UpdateAsync(user);
     }
 }

 متد 'AddToPreviousPasswordsAsync' کلمه عبور را در جدول 'PreviousPasswords' ذخیره می‌کند.

هرگاه کاربر سعی در بازنشانی یا تغییر کلمه عبورش دارد باید این متد را فراخوانی کنیم. API‌های لازم برای این کار در کلاس UserManager تعریف شده اند. باید این متدها را override کنیم و فراخوانی متد مذکور را پیاده کنیم. برای این کار کلاس ApplicationUserManager را باز کنید و متدهای ChangePassword و ResetPassword را بازنویسی کنید.
public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        private const int PASSWORD_HISTORY_LIMIT = 5;

        public ApplicationUserManager() : base(new ApplicationUserStore(new ApplicationDbContext()))
        {
            PasswordValidator = new CustomPasswordValidator(10);
        }

        public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword)
        {
            if (await IsPreviousPassword(userId, newPassword))
            {
                return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password"));
            }

            var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword);

            if (result.Succeeded)
            {
                var store = Store as ApplicationUserStore;
                await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword));
            }

            return result;
        }

        public override async Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword)
        {
            if (await IsPreviousPassword(userId, newPassword))
            {
                return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password"));
            }

            var result = await base.ResetPasswordAsync(userId, token, newPassword);

            if (result.Succeeded)
            {
                var store = Store as ApplicationUserStore;
                await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword));
            }

            return result;
        }

        private async Task<bool> IsPreviousPassword(string userId, string newPassword)
        {
            var user = await FindByIdAsync(userId);

            if (user.PreviousUserPasswords.OrderByDescending(x => x.CreateDate).
                Select(x => x.PasswordHash).Take(PASSWORD_HISTORY_LIMIT)
                .Where(x => PasswordHasher.VerifyHashedPassword(x, newPassword) != PasswordVerificationResult.Failed).Any())
            {
                return true;
            }

            return false;
        }
    }
فیلد 'PASSWORD_HISTORY_LIMIT' برای دریافت X رکورد از جدول تاریخچه کلمه عبور استفاده می‌شود. همانطور که می‌بینید از متد سازنده کلاس ApplicationUserStore برای گرفتن متد جدیدمان استفاده کرده ایم. هرگاه کاربری سعی می‌کند کلمه عبورش را بازنشانی کند یا تغییر دهد، کلمه عبورش را با 5 کلمه عبور قبلی استفاده شده مقایسه می‌کنیم و بر این اساس مقدار true/false بر می‌گردانیم.

کاربر جدیدی بسازید و به صفحه Manage  بروید. حال سعی کنید کلمه عبور را تغییر دهید و از کلمه عبور فعلی برای مقدار جدید استفاده کنید تا خطای اعتبارسنجی تولید شود. پیغامی مانند تصویر زیر باید دریافت کنید.

سورس کد این مثال را می‌توانید از این لینک دریافت کنید. نام پروژه Identity-PasswordPolicy است، و زیر قسمت Samples/Identity قرار دارد.

مطالب
بررسی Bad code smell ها: درخت ارث بری موازی یا Parallel inheritance hierarchy
این کد بد بو در دسته «جلوگیری کنندگان از تغییر» قرار می‌گیرد. اگر زمان ایجاد یک کلاس فرزند برای کلاسی، مجبور به ایجاد یک کلاس فرزند متناظر آن برای کلاس دیگری باشید، با این کد بد بود مواجه هستید. 
معمولا زمانی این اتفاق می‌افتد که یک درخت ارث بری به درخت ارث بری دیگری وابسته باشد. به‌طوری که هر یک از کلاس‌های موجود در آن، با یک کلاس در درخت دیگر متناظر باشند و ارتباط داشته باشند. این امر ایجاد تغییرات در کد را با مشکل مواجه خواهد کرد. 
به طور مثال:
public abstract class InvoiceGenerator 
{ 
    public abstract dynamic Generate(); 
} 
public abstract class AmountCalculator 
{ 
    public abstract decimal Calculate(dynamic item); 
}
public class StandardInvoiceGenerator : InvoiceGenerator 
{ 
    public override dynamic Generate() 
    { 
        return new object(); 
    } 
} 
public class StandardInvoiceAmountCalculator : AmountCalculator 
{ 
    public override decimal Calculate(dynamic item) 
    { 
        return 1; 
    } 
} 
public class PrepaymentInvoiceGenerator : InvoiceGenerator 
{ 
    public override dynamic Generate() 
    { 
        return new object(); 
    } 
} 
public class PrepaymentInvoiceAmountCalculator : AmountCalculator 
{ 
    public override decimal Calculate(dynamic item) 
    { 
        return 1; 
    } 
}

در این مثال کلاسی به نام InvoiceGenerator وجود دارد که کلاس پدر تولید کنندگان فاکتور است و کلاس دیگری نیز وجود دارد به نام AmountCalculator که کلاس پدر محاسبه قیمت است. هر یک از سازندگان فاکتور، یک کلاس متناظر در درخت Caluclator‌ها دارند؛ برای محاسبه قیمت آیتم‌های آن نوع فاکتور. به طور خاص در این مثال فاکتور استاندارد و فاکتور پیش پرداخت نوشته شده است  (در این مثال فقط یک سطح از ارث بری پیاده سازی شده است و تمامی کلاس‌ها مستقیما از کلاس پدر به ارث برده می‌شوند. در صورت وجود چند سطح ارث بری، مشکل پیچیده‌تر نیز خواهد شد).
حال فرض کنید قصد افزودن یک کلاس را برای ایجاد «فاکتور گزارش مخارج» (فاکتوری برای ثبت مخارجی که کارکنان زمان انجام کار برای کارفرما متحمل می‌شوند) دارید.
در این حالت نیاز است که یک کلاس ExpenseReportInvoiceGenerator ساخته شود و متناظر با آن نیاز خواهد بود کلاس ExpenseReportAmountCalculator ساخته شود. 

چرا چنین بویی به راه می‌افتد 

دلایل بوجود آمدن چنین بویی به سه دسته تقسیم می‌شوند: 

  • تشخیص نادرست روابط بین کلاس‌ها  
  • استفاده نامناسب از الگوهای طراحی 
  • تشخیص نادرست مسئولیت‌های کلاس‌ها و طراحی نامناسب آن 


روش‌های اصلاح این کد بد بو  

اصلاح چنین بوی بدی آسان نیست؛ زیرا درخت‌های ارث بری موازی، همیشه اتفاقی اشتباه نیستند و معمولا زمانیکه اصل Single responsibility در کدها به طور وسیعی رعایت شود، احتمال به‌وجود آمدن چنین الگویی وجود دارد؛ مانند مثالی که ذکر شد. در مثال مطرح شده اصل Single responsibility به درستی رعایت شده‌است. 
در برخورد با چنین بویی معمولا سه راه وجود دارد. 
اول:  عدم تغییر درخت‌های ارث بری موازی 
معمولا در مواردی مانند مثال ذکر شده در این مطلب، بهترین گزینه، عدم تغییر درخت ارث بری است. اگر رعایت کردن اصل single responsibility مهمتر از سادگی در ایجاد تغییرات باشد، این گزینه یک انتخاب خوب خواهد بود. یکی از مزایای دست نخوردن کدهایی مانند مثال مذکور، سهولت در نوشتن تست‌های واحد است. 

دوم: تبدیل درخت ارث بری به یک درخت ارث بری ناقص
در این روش می‌توان interface هایی را برای AmountCalculator و InvoiceGenerator ساخت و کلاس‌هایی که این دو interface را پیاده سازی می‌کنند؛ مانند: 
public interface IInvoiceGenerator 
{ 
    dynamic Generate(); 
} 
public interface IAmountCalculator 
{ 
    decimal Calculate(dynamic item); 
} 
public class StandardInvoiceGenerator : IInvoiceGenerator, IAmountCalculator 
{ 
    public dynamic Generate() 
    { 
        return new object(); 
    } 
    public decimal Calculate(dynamic item) 
    { 
        return 1; 
    } 
} 
public class PrepaymentInvoiceGenerator : IInvoiceGenerator, IAmountCalculator 
{ 
    public dynamic Generate() 
    { 
        return new object(); 
    } 
    public decimal Calculate(dynamic item) 
    { 
        return 1; 
    } 
}
با این تغییر، بخشی از اصل single responsibility نادیده گرفته شده‌است. اما با وجود یک درخت ارث بری فیزیکی، درخت منطقی موجود دست نخورده باقی مانده و در صورت نیاز می‌توان دوباره اقدام به جداسازی مسئولیت‌ها کرد. این جداسازی بیشتر اوقات زمانی اتفاق می‌افتد که یکی از جنبه‌های مسئولیتی یک کلاس، در جای دیگری به صورت مستقل مورد استفاده قرار گیرد. 

سوم: از بین بردن درخت ارث بری موازی 
این روش تقریبا مشابه روش ذکر شده در بخش دوم است. تنها با این تفاوت که در این روش، رابطه منطقی ارث بری موازی نیز حذف می‌شود؛ مانند:
public abstract class InvoiceGenerator 
{ 
    public abstract dynamic Generate(); 
    public abstract decimal CalculateItemAmount(dynamic item); 
} 
public class StandardInvoiceGenerator : InvoiceGenerator 
{ 
    public override dynamic Generate() 
    { 
        throw new NotImplementedException(); 
    } 
    public override decimal CalculateItemAmount(dynamic item) 
    { 
        throw new NotImplementedException(); 
    } 
} 
public class PrepaymentInvoiceGenerator : InvoiceGenerator 
{ 
    public override dynamic Generate() 
    { 
        throw new NotImplementedException(); 
    } 
    public override decimal CalculateItemAmount(dynamic item) 
    { 
        throw new NotImplementedException(); 
    } 
}
در این روش دو درخت ارث بری از نظر منطقی و فیزیکی با یکدیگر ادغام شده‌اند. البته همان طور که در بخش یک گفته شد، بهترین گزینه برای مثال ذکر شده در این مطلب، دست نخوردن آن است. این روش زمانی مناسب است که موجودیت‌های ادغام شده از نظر ذاتی به قدری به یکدیگر شباهت داشته باشند که عملا بتوانیم آن‌ها را یک موجودیت مستقل در نظر بگیریم. 

جمع بندی 

روش‌های رفع بوی بد «درخت‌های ارث بری موازی» به زمینه ایجاد آنها وابستگی شدیدی دارد. به همین دلیل نمی‌توان راه حلی کلی را برای آن ارائه داد. سه روش ذکر شده در این مطلب در واقع سه نوع رفتاری هستند که می‌توان با چنین بوی بدی داشت. بسته به زمینه ایجاد این بوی بد و شرایط کد می‌توان هر یک از این روش‌ها را انتخاب نمود. نکاتی مانند سهولت نگهداری، تست نویسی آسان و خوانایی کد، فاکتورهایی هستند که می‌توانند در تصمیم گیری مد نظر قرار گیرند.
نظرات مطالب
استفاده از لوسین برای برجسته سازی عبارت جستجو شده در نتایج حاصل
چطور می‌شود چندین جدول مختلف رو ایندکس کرد؟ هربار ایندکس میکنم اطلاعات قبلی از بین میره
 private static FSDirectory _shopDirectory;

 private static FSDirectory ShopDirectory
        {
            get
            {
                if (_shopDirectory == null)
                    _shopDirectory = FSDirectory.Open(new DirectoryInfo(ShopPath));
                if (IndexWriter.IsLocked(_shopDirectory))
                    IndexWriter.Unlock(_shopDirectory);
                var lockFilePath = Path.Combine(ShopPath, "write.lock");
                if (File.Exists(lockFilePath))
                    File.Delete(lockFilePath);
                return _shopDirectory;
            }
        }

 public static void CreateProducSkustIndex(List<ProductSkuLuceneViewModel> productSkuLucenes)
        {
            var directory = ShopDirectory;
            var analyzer = new StandardAnalyzer(_version, PersianStopwords.List);
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                productSkuLucenes.ForEach(row => writer.AddDocument(MapProductToDocument(row)));

                writer.Optimize();
                writer.Commit();
                writer.Close();
            }
        }
 public static void CreateProducCategoriestIndex(List<CategoryLuceneViewModel> categoryLucenes)
        {
            var directory = ShopDirectory;
            var analyzer = new StandardAnalyzer(_version, PersianStopwords.List);
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                categoryLucenes.ForEach(row => writer.AddDocument(MapProductCategoryToDocument(row)));

                writer.Optimize();
                writer.Commit();
                writer.Close();
            }
        }

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

اشتباه 1:
استفاده از

throw ex;
بجای استفاده از
throw;
در حالت اول، تمام stack trace موجود تا نقطه‌ی فراخوانی دستور ذکر شده پاک خواهند شد، اما در حالت دوم stack trace‌ حفظ شده و دیباگ کردن کد را ساده می‌کند.

اشتباه 2:
درک اشتباه عملکرد متد replace :
string s = "Take this out";
s.Replace("this", "that"); //wrong
بجای استفاده از :
s = s.Replace("this", "that");  //correct

اگر از fxCop استفاده کنید، اینگونه خطاها را (عدم استفاده از مقدار بازگشتی) گوشزد می‌کند.

اشتباه 3:
استفاده‌ی بی دقت از متغیرهای استاتیک در یک برنامه وب. دو مثال زیر را در نظر بگیرید:
public static string GetCookieName(Cookie c)
{
return c.Name;
}

static List<string> cookieList = new List<string>();
public static void AddToCookieList(Cookie c)
{
cookieList.Add(c.Name);

}

برنامه‌های وب ذاتا چند ریسمانی هستند و زمانیکه یک متغیر را از نوع استاتیک تعریف می‌کنید، این متغیر، هنگام مراجعه‌ی کاربران بین آن‌ها به اشتراک گذاشته می‌شود و امکان تخریب یا استفاده‌ی ناصحیح از مقادیر آن‌ها وجود خواهد داشت. در حالت اول نیازی به مباحث همزمانی و قفل کردن منابع نیست زیرا متغیری که در متد استفاده می‌شود، thread safe است اما cookieList در مثال دوم خیر و حتما هنگام استفاده از آن باید مباحث قفل کردن منابع را درنظر داشت. (حتی اگر برنامه‌ی شما از نوع وبی هم نیست اما چند ریسمانی است این مطلب باز هم صادق می‌باشد)


اشتباه 4:
مقابله با خطاها به شکلی نادرست: (اصطلاحا خفه کردن خطاها!)
 try
{
//something
File.Delete(blah);
}
catch{}
در این حالت اگر خطایی رخ دهد کاربر متوجه نخواهد شد دقیقا مشکلی وجود داشته یا خیر و علت آن چیست.
هرچند گاهی از اوقات اگر خطای حاصل برای ما اهمیتی نداشت می‌توان از آن استفاده نمود، در غیراینصورت باید حتما از این روش پرهیز کرد.

اشتباه 5:
ارائه‌ی برنامه‌های ASP.Net با گزینه‌ی پیش فرض Debug=true در web.config که پیشتر در مورد آن در این سایت بحث شده است.


اشتباه 6:
عدم استفاده از امکانات ویژه‌ی دات نت فریم ورک هنگام کار با رشته‌ها:
string s = "This ";
s += "is ";
s += "not ";
s += "the ";
s += "best ";
s += "way.";

StringBuilder sb = new StringBuilder();
sb.Append("This ");
sb.Append("is ");
sb.Append("much ");
sb.Append("better. ");

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

اشتباه 7:
عدم استفاده از عبارت using هنگام استفاده از اشیایی از نوع Idisposable . مثالی در این مورد.

using (StreamReader reader=new StreamReader(file))
{
//your code here

}

اشتباه 8:
فراموش کردن بررسی نال بودن یک شیء هنگام استفاده از آن.
string val = Session["xyz"].ToString();

این نوع کد نویسی یکی از اشتباهات متداول تمامی تازه واردان به ASP.Net است. حتما باید پیش از استفاده از متد ToString بررسی شود که آیا این سشن نال است یا نه. در غیراینصورت حاصل کار فقط یک exception خواهد بود. (استفاده از افزونه‌ی ری شارپر در این موارد کمک بزرگی است، زیرا به محض قرار گرفتن مکان نما روی شیءایی که احتمال نال بودن آن میسر است، یک راهنما را به شما ارائه خواهد کرد)

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

مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت یازدهم
در این قسمت قصد بررسی کامپوننت‌های فوق العاده‌ی Syncfusion را داریم. احتمالا Syncfusion را با کتاب‌های Succinctly Series آن می شناسید. این شرکت برای Xamarin Forms نزدیک به 130‌ کامپوننت، شامل موارد کار با دیتا، اعم از فرم‌های Data Entry ،Data Grid و ListView را نوشته که در کنار کنترل‌های کار با فایل‌های Office-PDF و همچنین گزارشات و چارت‌ها و سایر کنترل‌های آن، نیاز هر برنامه‌ای را برآورده می‌کند. یکی از چند ده کتاب Xamarin Forms نیز توسط این شرکت نوشته شده‌است.
ما ضمن استفاده کامپوننت List View آن، هم کار با List View را یاد می‌گیریم و هم کار با Syncfusion را. در نظر داشته باشید که خود Xamarin Forms نیز List View دارد و در نسخه‌ی 4 آن که هم اکنون در مرحله‌ی Preview است، کنترل جدیدی به نام CollectionView نیز ارائه شده که امکانات خیلی خوبی دارد.
توجه: مطالب آموزش زیر، از این لینک آورده شده‌است.

برای شروع، ابتدا Nuget Package مربوطه را در پروژه‌ی XamApp نصب کنید. نیازی به نصب کردن آن در XamApp.Android و XamApp.iOS و XamApp.UWP نیست، ولی باز و بسته کردن ویژوال استودیو بعد از نصب و راست کلیک کردن بر روی Solution و زدن Restore nuget packages ایده‌ی خوبی است!

در پروژه‌ی iOS، در فایل AppDelegate.cs، بعد از Forms.Init، کد زیر را کپی کنید:

SfListViewRenderer.Init();

همین کد را در MainPage.xaml.cs در پروژه UWP، قبل از LoadApplication قرار دهید. نیازی به انجام کاری در Android نیست.

سپس Product Key این محصول را به دست آورده و در پروژه XamApp، فولدر Views در فایل SyncfusionLicense قرار دهید.

حال برای نمایش لیستی از محصولات، ابتدا کلاس Product را ایجاد می‌کنیم. چه در زمانیکه یک Rest api را در سمت سرور فراخوانی می‌کنیم و چه زمانیکه با دیتابیس بر روی گوشی یعنی Sqlite کار می‌کنیم، در نهایت لیستی از یک کلاس را داریم (در اینجا Product).

    public class Product : Bindable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsActive { get; set; }
        public decimal Price { get; set; }
    }

در یک View Model جدید با نام ProductsViewModel، در OnNavigatedToAsync، دیتا را از سرور یا دیتابیس، بر روی گوشی دریافت می‌کنیم؛ اما در این مثال، برای راحتی بیشتر یک List را New می‌کنیم:

    public class ProductsViewModel : BitViewModelBase
    {
        public List<Product> Products { get; set; }

        public async override Task OnNavigatedToAsync(INavigationParameters parameters)
        {
            Products = new List<Product> // getting products from server or sqlite database
            {
                new Product { Id = 1, IsActive = true, Name = "Product1" , Price = 12.2m /* m => decimal */ },
                new Product { Id = 2, IsActive = false, Name = "Product2" , Price = 14 },
                new Product { Id = 3, IsActive = true, Name = "Product3" , Price = 11 },
            };
            await base.OnNavigatedToAsync(parameters);
        }
    }

حال نوبت به دادن یک Template می‌رسد. مثلا فرض کنید می‌خواهیم نام را درون یک Label نمایش دهیم و بر اساس فعال یا غیر فعال بودن Product، یک Checkbox را تغییر داده، تیک بزنیم یا نزنیم و در نهایت نمایش قیمت را در یک Label دیگر خواهیم داشت.

    <sfListView:SfListView ItemsSource="{Binding Products}">
        <sfListView:SfListView.ItemTemplate>
            <DataTemplate>
                <FlexLayout x:DataType="model:Product" Direction="Row">
                    <Label
                        FlexLayout.Basis="50%"
                        Text="{Binding Name}"
                        VerticalTextAlignment="Center" />
                    <bitControls:BitCheckbox InputTransparent="True" FlexLayout.Basis="25%" IsChecked="{Binding IsActive}" />
                    <Label
                        FlexLayout.Basis="25%"
                        Text="{Binding Price}"
                        VerticalTextAlignment="Center" />
                </FlexLayout>
            </DataTemplate>
        </sfListView:SfListView.ItemTemplate>
    </sfListView:SfListView>

همانطور که می‌بینید، در DataTemplate از Flex Layout استفاده شده است. Flex Layout در کنار Grid, Stack, Relative, Absolute و سایر Layout‌‌های Xamarin Forms در پروژه قابلیت استفاده دارد و مزیت‌های خاص خود را دارد.

این Data Template توسط List View، حداکثر سه بار ساخته می‌شود؛ چون View Model در لیست مثال خود، سه Product دارد. خود List View تکنیک‌های Virtualization و Cell Reuse را بدون نیاز به هیچ کد اضافه‌ای هندل می‌کند و Performance خوبی دارد. در View مربوطه یعنی ProductsView.xaml، هر Binding ای (مثل Binding Products) به View Model اشاره می‌کند، اما درون Data Template، هر Binding به Product ای اشاره می‌کند که آن ردیف List View، دارد نمایش‌اش می‌دهد. برای همین x:DataType را روی Flex Layout درون Data Template به Product وصل کرده‌ایم. در این صورت اگر بنویسیم Binding N_ame، به ما خطا داده می‌شود که کلاس Product هیچ Property با نام N_ame ندارد که خطای درستی است.

روی BitCheckbox مقدار InputTransparent را برابر با True داده‌ایم که باعث می‌شود کلیک روی Checkbox عملا در نظر گرفته نشود. این منطقی است، زیرا عوض کردن مقدار Checkbox در این مثال ما ذخیره نمی‌شود و کاربرد نمایشی دارد و فقط باعث گیج شدن کاربر می‌شود.

کنترل BitCheckbox از مجموعه کنترل‌های Bit است که اخیرا با BitDatePicker آن آشنا شده‌اید. برای آشنایی با نحوه افزودن این کنترل‌ها به یک پروژه، به مستندات Bit Framework مراجعه کنید. خود Syncfusion نیز Checkbox دارد.

حال فرض کنید که قرار است دکمه‌ای برای هر ردیف List View داشته باشیم که با زدن روی آن، اطلاعات Product به سرور ارسال شود و جزئیات بیشتری دریافت و در قالب یک Alert نمایش داده شود.  برای این کار، ابتدا به Data Template که Flex Layout است، یک دکمه اضافه می‌کنیم. سپس Command آن دکمه را به View Model بایند می‌کنیم. در آن Command البته احتیاج داریم بدانیم درخواست نمایش جزئیات بیشتر، برای کدام Product داده شده. این مهم با Command Parameter شدنی است.

برای پیاده سازی این مثال، در سمت View Model داریم:

 
public BitDelegateCommand<Product> ShowProductDetailsCommand { get; set; }
public IUserDialogs UserDialogs { get; set; } async Task ShowProductDetails(Product product) { string productDetail = $"Product: {product.Name}'s more info: ..."; // get more info from server. await UserDialogs.AlertAsync(productDetail, "Product Detail"); }

کامند ShowProductDetailCommand یک پارامتر را از جنس Product می‌گیرد و آن Product ای است که روی دکمه آن کلیک شده‌است. با Clone کردن آخرین نسخه XamApp و درخواست نمایش صفحه‌ی Products در App.xaml.cs به صورت زیر و اجرای برنامه، می‌توانید درک بهتری از عملکرد آن داشته باشید:

await NavigationService.NavigateAsync("/Nav/Products", animated: false);

سپس در View مربوطه داریم:

 
...
<Button Command="{Binding ShowProductDetailsCommand}" CommandParameter="{Binding .}" Text="Detail..." /> </FlexLayout> </DataTemplate>

CommandParameter اگر برابر با Binding Id می‌بود، به Command در سمت View Model، بجای کل Product، فقط Id آن ارسال می‌شد. ولی Show Product Detail Command منتظر یک Product کامل است، نه فقط Id آن. با نوشتن 

CommandParameter="{Binding .}"

کل Product با کلیک روی دکمه به Command ارسال می‌شود.

اکنون اگر پروژه را Build کنید، خطایی را از x:DataType خواهید گرفت که منطقی است. اگر Binding Name و Binding Price دو Property با نام‌های Name و Price را از کلاس Product جستجو می‌کنند، پس قاعدتا ShowProductDetailCommand نیز در همان کلاس مدل، یعنی Product جستجو می‌شود! ولی می‌دانیم که این Command در View Model ما یعنی ProductsViewModel است. برای حل این مشکل، به جای Binding از bit:ViewModelBinding استفاده می‌کنیم:

Command="{bit:ViewModelBinding ShowProductDetailsCommand}"

در این صورت، بجای جستجو کردن ShowProductDetailCommand در کلاس Product، این را در ProductsViewModel جستجو می‌کند که منجر به خروجی درست می‌شود.

این List View دارای امکاناتی چون Infinite loading، Pull to refresh و Grouping-Sorting-Filtering و ... است که می‌توانید از روی مستندات خوب Syncfusion، آنها را راه اندازی کنید و اگر به مشکلی برخوردید نیز اینجا بپرسید. همچنین نگاهی به لیست 129 کنترل دیگر بیاندازید و ببینید که در برنامه‌های خود از کدام یک از آنها می‌توانید استفاده کنید.