اشتراک‌ها
کتابخانه fontfaceobserver

Font Face Observer is a small @font-face loader and monitor (3.5KB minified and 1.3KB gzipped) compatible with any webfont service. It will monitor when a webfont is loaded and notify you. It does not limit you in any way in where, when, or how you load your webfonts. Unlike the Web Font Loader Font Face Observer uses scroll events to detect font loads efficiently and with minimum overhead.

var font = new FontFaceObserver('My Family', {
  weight: 400
});

font.load().then(function () {
  console.log('Font is available');
}, function () {
  console.log('Font is not available');
});
$ npm install fontfaceobserver
کتابخانه fontfaceobserver
مطالب
مقدار دهی کلیدهای خارجی در NHibernate و Entity framework

ORM های NHibernate و Entity framework روش‌های متفاوتی را برای به روز رسانی کلید خارجی با حداقل رفت و برگشت به دیتابیس ارائه می‌دهند که در ادامه معرفی خواهند شد.

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

الف) بررسی مدل برنامه



در اینجا جهت تعریف ویژگی‌ها یا Attributes تعریف شده در این کلاس‌ها از NHibernate validator استفاده شده (+). مزیت اینکار هم علاوه بر اعتبارسنجی سمت کلاینت (پیش از تبادل اطلاعات با بانک اطلاعاتی)، تولید جداولی با همین مشخصات است. برای مثال Fluent NHibernate بر اساس ویژگی Length تعریف شده با طول حداکثر 120 ، یک فیلد nvarchar با همین طول را ایجاد می‌کند.

public class Account
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}

public class Category
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 130, Message = "طول نام باید بین 3 و 130 کاراکتر باشد")]
public virtual string Name { get; set; }
}

public class Payee
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}

public class Bill
{
public virtual int Id { get; set; }

[NotNull]
public virtual Account Account { get; set; }

[NotNull]
public virtual Category Category { get; set; }

[NotNull]
public virtual Payee Payee { get; set; }

[NotNull]
public virtual decimal Amount { set; get; }

[NotNull]
public virtual DateTime BillDate { set; get; }

[NotNullNotEmpty]
[Length(Min = 1, Max = 500, Message = "طول توضیحات باید بین 1 و 500 کاراکتر باشد")]
public virtual string Description { get; set; }
}




ب) ساختار جداول متناظر (تولید شده به صورت خودکار توسط Fluent NHibernate در اینجا)


در مورد نحوه‌ی استفاده از ویژگی AutoMapping و همچنین تولید خودکار ساختار بانک اطلاعاتی از روی جداول در NHibernate قبلا توضیح داده شده است. البته بدیهی است که ترکیب مقاله‌ی Validation و آشنایی با AutoMapping در اینجا جهت اعمال ویژگی‌ها باید بکار گرفته شود که در همان مقاله‌ی Validation مفصل توضیح داده شده است.
نکته‌ی مهم database schema تولیدی، کلید‌های خارجی (foreign key) تعریف شده بر روی جدول Bills است (همان AccountId، CategoryId و PayeeId تعریف شده) که به primary key جداول متناظر اشاره می‌کند.
    create table Accounts (
AccountId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (AccountId)
)

create table Bills (
BillId INT IDENTITY NOT NULL,
Amount DECIMAL(19,5) not null,
BillDate DATETIME not null,
Description NVARCHAR(500) not null,
AccountId INT not null,
CategoryId INT not null,
PayeeId INT not null,
primary key (BillId)
)

create table Categories (
CategoryId INT IDENTITY NOT NULL,
Name NVARCHAR(130) not null,
primary key (CategoryId)
)

create table Payees (
PayeeId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (PayeeId)
)

alter table Bills
add constraint fk_Account_Bill
foreign key (AccountId)
references Accounts

alter table Bills
add constraint fk_Category_Bill
foreign key (CategoryId)
references Categories

alter table Bills
add constraint fk_Payee_Bill
foreign key (PayeeId)
references Payees

ج) صفحه‌ی ثبت صورتحساب‌ها

صفحات ثبت گروه‌های اقلام، حساب‌ها و فروشنده‌ها، نکته‌ی خاصی ندارند. چون این جداول وابستگی خاصی به جایی نداشته و به سادگی اطلاعات آن‌ها را می‌توان ثبت یا به روز کرد.
صفحه‌ی مشکل در این مثال، همان صفحه‌ی ثبت صورتحساب‌ها است که از سه کلید خارجی به سه جدول دیگر تشکیل شده است.
عموما برای طراحی این نوع صفحات، کلیدهای خارجی را با drop down list نمایش می‌دهند و اگر در جهت سهولت کار کاربر قدم برداشته شود، باید از یک Auto complete drop down list استفاده کرد تا کاربر برنامه جهت یافتن آیتم‌های از پیش تعریف شده کمتر سختی بکشد.



اگر از Silverlight یا WPF استفاده شود، امکان بایند یک لیست کامل از اشیاء با تمام خواص مرتبط به آن‌ها وجود دارد (هر رکورد نمایش داده شده در دراپ داون لیست، دقیقا معادل است با یک شیء متناظر با کلاس‌های تعریف شده است). اگر از ASP.NET استفاده شود (یعنی یک محیط بدون حالت که پس از نمایش یک صفحه دیگر خبری از لیست اشیاء بایند شده وجود نخواهد داشت و همگی توسط وب سرور جهت صرفه جویی در منابع تخریب شده‌اند)، بهتر است datatextfield را با فیلد نام و datavaluefield را با فیلد Id مقدار دهی کرد تا کاربر نهایی، نام را جهت ثبت اطلاعات مشاهده کند و برنامه از Id موجود در لیست جهت ثبت کلیدهای خارجی استفاده نماید.
و نکته‌ی اصلی هم همینجا است که چگونه؟! چون ما زمانیکه با یک ORM سر و کار داریم، برای ثبت یک رکورد در جدول Bills باید یک وهله از کلاس Bill را ایجاد کرده و خواص آن‌را مقدار دهی کنیم. اگر به تعریف کلاس Bill مراجعه کنید، سه خاصیت آن از نوع سه کلاس مجزا تعریف شده است. به به عبارتی با داشتن فقط یک id از رکوردهای این کلاس‌ها باید بتوان سه وهله‌ی متناظر آن‌ها را از بانک اطلاعاتی خواند و سپس به این خواص انتساب داد:

var newBill = new Bill
{
Account = accountRepository.GetByKey(1),
Amount = 1,
BillDate = DateTime.Now,
Category = categoryRepository.GetByKey(1),
Description = "testestest...",
Payee = payeeRepository.GetByKey(1)
};
یعنی برای ثبت یک رکورد در جدول Bills فوق، چهار بار رفت و برگشت به دیتابیس خواهیم داشت:
- یکبار برای دریافت رکورد متناظر با گروه‌ها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت می‌شود)
- یکبار برای دریافت رکورد متناظر با فروشند‌ه‌ها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت می‌شود)
- یکبار برای دریافت رکورد متناظر با حساب‌ها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت می‌شود)
- یکبار هم ثبت نهایی اطلاعات در بانک اطلاعاتی

متد GetByKey فوق همان متد session.Get استاندارد NHibernate است (چون به primary key ها از طریق drop down list دسترسی داریم، به سادگی می‌توان بر اساس متد Get استاندارد ذکر شده عمل کرد).

SQL نهایی تولیدی هم به صورت واضحی این مشکل را نمایش می‌دهد (4 بار رفت و برگشت؛ سه بار select یکبار هم insert نهایی):
SELECT account0_.AccountId as AccountId0_0_, account0_.Name as Name0_0_
FROM Accounts account0_ WHERE account0_.AccountId=@p0;@p0 = 1 [Type: Int32 (0)]

SELECT category0_.CategoryId as CategoryId2_0_, category0_.Name as Name2_0_
FROM Categories category0_ WHERE category0_.CategoryId=@p0;@p0 = 1 [Type: Int32 (0)]

SELECT payee0_.PayeeId as PayeeId3_0_, payee0_.Name as Name3_0_
FROM Payees payee0_ WHERE payee0_.PayeeId=@p0;@p0 = 1 [Type: Int32 (0)]

INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:48:33 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]

کسانی که قبلا با رویه‌های ذخیره شده کار کرده باشند (stored procedures) احتمالا الان خواهند گفت؛ ما که گفتیم این روش کند است! سربار زیادی دارد! فقط کافی است یک SP بنویسید و کل عملیات را با یک رفت و برگشت انجام دهید.
اما در ORMs نیز برای انجام این مورد در طی یک حرکت یک ضرب راه حل‌هایی وجود دارد که در ادامه بحث خواهد شد:

د) پیاده سازی با NHibernate
برای حل این مشکل در NHibernate با داشتن primary key (برای مثال از طریق datavaluefield ذکر شده)، بجای session.Get از session.Load استفاده کنید.
session.Get یعنی همین الان برو به بانک اطلاعاتی مراجعه کن و رکورد متناظر با کلید اصلی ذکر شده را بازگشت بده و یک شیء از آن را ایجاد کن (حالت‌های دیگر دسترسی به اطلاعات مانند استفاده از LINQ یا Criteria API یا هر روش مشابه دیگری نیز در اینجا به همین معنا خواهد بود).
session.Load یعنی فعلا دست نگه دار! مگر در جدول نهایی نگاشت شده، اصلا چیزی به نام شیء مثلا گروه وجود دارد؟ مگر این مورد واقعا یک فیلد عددی در جدول Bills بیشتر نیست؟ ما هم که الان این عدد را داریم (به کمک عناصر دراپ داون لیست)، پس لطفا در پشت صحنه یک پروکسی برای ایجاد شیء مورد نظر ایجاد کن (uninitialized proxy to the entity) و سپس عملیات مرتبط را در حین تشکیل SQL نهایی بر اساس این عدد موجود انجام بده. یعنی نیازی به رفت و برگشت به بانک اطلاعاتی نیست. در این حالت اگر SQL نهایی را بررسی کنیم فقط یک سطر زیر خواهد بود (سه select ذکر شده حذف خواهند شد):
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:58:22 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]

ه) پیاده سازی با Entity framework

Entity framework زمانیکه بانک اطلاعاتی فوق را (به روش database first) به کلاس‌های متناظر تبدیل/نگاشت می‌کند، حاصل نهایی مثلا در مورد کلاس Bill به صورت خلاصه به شکل زیر خواهد بود:
public partial class Bill : EntityObject
{
public global::System.Int32 BillId {set;get;}
public global::System.Decimal Amount {set;get;}
public global::System.DateTime BillDate {set;get;}
public global::System.String Description {set;get;}
public global::System.Int32 AccountId {set;get;}
public global::System.Int32 CategoryId {set;get;}
public global::System.Int32 PayeeId {set;get;}
public Account Account {set;get;}
public Category Category {set;get;}
}
به عبارتی فیلدهای کلیدهای خارجی، در تعریف نهایی این کلاس هم مشاهده می‌شوند. در اینجا فقط کافی است سه کلید خارجی، از نوع int مقدار دهی شوند (و نیازی به مقدار دهی سه شیء متناظر نیست). در این حالت نیز برای ثبت اطلاعات، فقط یکبار رفت و برگشت به بانک اطلاعاتی خواهیم داشت.

مطالب
روشی برای مقایسه‌ی مقادیر تمام خواص دو شیء در آزمون‌های واحد
در زمان نوشتن تست‌های مختلف (Unit - Integration - UI) گاهی اوقات پیش می‌آید که بخواهید تمامی خصوصیت‌های یک شیء را تایید کنید. معمولا نوشتن اعتبارسنجی برای همه خصوصیت‌ها و همین طور پیام‌های استثناء برای هر یک در زمان عدم تایید اعتبار، کار بسیار زمانبری است. در این مقاله به شما نشان خواهم داد که چگونه با نوشتن یک اعتبارسنج عمومی از اتلاف زمان زیادی جلوگیری کنید.

با استفاده از کلاس زیر می‌توان کار اعتبارسنجی را با استفاده از Reflection به راحتی انجام داد. در اینجا برای اعتبارسنجی DateTime از کلاس DateTimeAssert استفاده کرده‌ایم.
public class PropertiesValidator<TK, T> where T : new() where TK : new()
{
    static TK _instance;

    public static TK Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new TK();
            }
            return _instance;
        }
    }

    public void Validate(T expectedObject, T realObject, params string[] propertiesNotToCompare)
    {
        var properties = realObject.GetType().GetProperties();
        foreach (var currentRealProperty in properties)
        {
            if (!propertiesNotToCompare.Contains(currentRealProperty.Name))
            {
                var currentExpectedProperty = expectedObject.GetType().GetProperty(currentRealProperty.Name);
                var exceptionMessage = $"The property {currentRealProperty.Name} of class {currentRealProperty.DeclaringType?.Name} was not as expected.";

                if (currentRealProperty.PropertyType != typeof(DateTime) && currentRealProperty.PropertyType != typeof(DateTime?))
                {
                    Assert.AreEqual( currentExpectedProperty.GetValue( expectedObject,
                                                                        null ),
                                        currentRealProperty.GetValue( realObject,
                                                                    null ),
                                        exceptionMessage );
                }
                else
                {
                    DateTimeAssert.Validate( currentExpectedProperty.GetValue( expectedObject,
                                                                                null ) as DateTime?,
                                                currentRealProperty.GetValue( realObject,
                                                                            null ) as DateTime?,
                                                TimeSpan.FromMinutes( 5 ) );
                }
            }
        }
    }
}


طرز استفاده

فرض کنید مدلی داریم با این مشخصات:
public class ObjectToAssert
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime LastVisit { get; set; }
}
و دو نمونه از آن را ایجاد کرده ایم:
var expectedObject = new ObjectToAssert
                        {
                            FirstName = "Vahid",
                            LastName = "Mohammad Taheri",
                            LastVisit = new DateTime( 2016, 11, 14, 0, 10, 50 )
                        };
var actualObject = new ObjectToAssert
                        {
                            FirstName = "Vahid",
                            LastName = "Mohammad Taheri",
                            LastVisit = new DateTime( 2016, 11, 14, 0, 13, 50 )
                        };
کلاسی را با ارث بری از PropertiesValidator ایجاد می‌کنیم:
public class ObjectToAssertValidator : PropertiesValidator<ObjectToAssertValidator, ObjectToAssert>
{
    public void Validate(ObjectToAssert expected, ObjectToAssert actual)
    {
        this.Validate(expected, actual, "FirstName");
    }
}

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


اکنون دو نمونه ساخته شده از ObjectToAssert بالا را با فراخوانی دستور زیر اعتبارسنجی می‌کنیم:
ObjectToAssertValidator.Instance.Validate(expectedObject, actualObject);
مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت اول

موجودیت‌های زیر را در نظر بگیرید: 

public class Customer
{
    public Customer()
    {
        Orders = new ObservableCollection<Order>();
    }
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public string FullName
    {
        get
        {
            return Name + " " + Family;
        }
    }

    public virtual IList<Order> Orders { get; set; }
}
public class Product
{
    public Product()
    {
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
public class OrderDetail
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Count { get; set; }
    public Guid OrderId { get; set; }
    public int Price { get; set; }

    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }

    public string ProductName
    {
        get
        {
            return Product != null ? Product.Name : string.Empty;
        }
    }
}
public class Order
{
    public Order()
    {
        OrderDetail = new ObservableCollection<OrderDetail>();
    }
    public Guid Id { get; set; }
    public DateTime Date { get; set; }

    public Guid CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual IList<OrderDetail> OrderDetail { get; set; }

    public string CustomerFullName
    {
        get
        {
            return Customer == null ? string.Empty : Customer.FullName;
        }
    }

    public int TotalPrice
    {
        get
        {
            if (OrderDetail == null)
                return 0;

            return
                OrderDetail.Where(orderdetail => orderdetail.Product != null)
                .Sum(orderdetail => orderdetail.Price*orderdetail.Count);
        }
    }
}

و نگاشت موجودیت ها: 

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.Id);
        Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        HasKey(p => p.Id);
        Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail>
{
    public OrderDetailConfiguration()
    {
        HasKey(od => od.Id);
        Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderConfiguration: EntityTypeConfiguration<Order>
{
    public OrderConfiguration()
    {
        HasKey(o => o.Id);
        Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}

و برای معرفی موجودیت‌ها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف می‌کنیم:

public class StoreDbContext : DbContext
{
    public StoreDbContext()
        : base("name=StoreDb")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerConfiguration());
        modelBuilder.Configurations.Add(new OrderConfiguration());
        modelBuilder.Configurations.Add(new OrderDetailConfiguration());
        modelBuilder.Configurations.Add(new ProductConfiguration());
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderDetail> OrderDetails { get; set; }
}

جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف می‌کنیم:

public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext>
{
    protected override void Seed(StoreDbContext context)
    {
        var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" };
        var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" };
        var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" };

        var product1 = new Product {Name = "CPU", Price = 350000};
        var product2 = new Product {Name = "Monitor", Price = 500000};
        var product3 = new Product {Name = "Keyboard", Price = 30000};
        var product4 = new Product {Name = "Mouse", Price = 20000};
        var product5 = new Product {Name = "Power", Price = 70000};
        var product6 = new Product {Name = "Hard", Price = 250000};

        var order1 = new Order
        {
            Customer = customer1, Date = new DateTime(2013, 1, 1),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 1, Price = product1.Price},
                new OrderDetail {Product = product2, Count = 1, Price = product2.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
            }
        };

        var order2 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 5),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 2, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 4, Price = product3.Price},
            }
        };

        var order3 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 4, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 5, Price = product3.Price},
                new OrderDetail {Product = product5, Count = 6, Price = product5.Price},
            }
        };

        var order4 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
                new OrderDetail {Product = product6, Count = 1, Price = product6.Price},
            }
        };

        var order5 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 12),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product5, Count = 2, Price = product5.Price},
                new OrderDetail {Product = product6, Count = 5, Price = product6.Price},
            }
        };

        context.Customers.Add(customer3);

        context.Orders.Add(order1);
        context.Orders.Add(order2);
        context.Orders.Add(order3);
        context.Orders.Add(order4);
        context.Orders.Add(order5);

        context.SaveChanges();
    }

و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار می‌دهیم:

Database.SetInitializer(new MyTestDb());

در انتها ConnectionString را در App.Config به صورت زیر تعریف می‌کنیم:

<connectionStrings>
    <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS;
Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/>
</connectionStrings>

بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:

using (var context = new StoreDbContext())
{
    var query = context.Customers;

    foreach (var customer in query)
    {
        Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                  customer.Name, customer.Family);
    }
}

پرس و جوی تعریف شده لیست تمام Customer‌ها را باز می‌گرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا می‌شود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق می‌افتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده می‌شود. EF در این حالت تمام داده‌ها را در یک لحظه باز نمی‌گرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام داده‌ها از database واکشی شود. خروجی به صورت زیر خواهد بود:

Customer Name: Vahid, Customer Family: Nasiri
Customer Name: Mohsen, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari

نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده می‌شود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();

خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.

پرس و جوی زیر مشتریهایی که نام آنها Mohsen هست را باز می‌گرداند:

private static void Query3()
{
    using (var context = new StoreDbContext())
    {
        var methodSyntaxquery = context.Customers
                   .Where(c => c.Name == "Mohsen");
        var sqlSyntaxquery = from c in context.Customers
                             where c.Name == "Mohsen"
                             select c;

        foreach (var customer in methodSyntaxquery)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                      customer.Name, customer.Family);
        }
    }

    // Output:
    // Customer Name: Mohsen, Customer Family: Jamshidi
    // Customer Name: Mohsen, Customer Family: Akbari
}

همانطور که مشاهده می‌کنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.

روش Method Syntax روشی است که از متدهای الحاقی (Extention Method)  ‌و عبارت‌های لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده می‌شود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد می‌شود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.

اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet می‌باشد. DbSet کلاس و اینترفیس‌های متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:

  • IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم می‌کند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده می‌کند. بنابراین تمامی متدهایی که در LINQ To Object استفاده می‌شوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود. 
LINQ to Entities does not recognize the method 'Store.Model.Customer LastOrDefault[Customer](System.Linq.IQueryable`1[Store.Model.Customer], System.Linq.Expressions.Expression`1[System.Func`2[Store.Model.Customer,System.Boolean]])' method, and this method cannot be translated into a store expression.
  • <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local می‌باشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده می‌شوند که  در ادامه توضیح داده خواهند شد.
  • <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include می‌باشد و در ادامه توضیح داده خواهند شد.

متد Find: این متد کلید اصلی را به عنوان ورودی گرفته و برای بازگرداندن نتیجه مراحل زیر را طی می‌کند:
  1. داده‌های موجود در حافظه را بررسی می‌کند یعنی آنهایی که Load و یا Attach شده اند.
  2. داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
  3. داده هایی که در database هستند ولی هنوز Load نشده اند.
Find در صورت پیدا نکردن Exception ای صادر نمی‌کند بلکه مقدار null را بر می‌گرداند.
private static void Query4()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367"));

        if (customer == null)
            Console.WriteLine("Customer not found");
        else
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
    }
}
با توجه به اینکه Id‌ها توسط Database ساخته می‌شوند. شما باید از Id دیگری که موجود می‌باشد، استفاده کنید تا نتیجه ای برگشت داده شود.
نکته: در صورتیکه کلید اصلی شما از دو یا چند فیلد تشکیل شده بود. می‌بایست این دو یا چند مقدار را به عنوان پارامتر به Find بفرستید.

متد Single: گاهی نیاز هست که داده‌ای پرس و جو شود اما نه با کلید اصلی بلکه با شرط دیگری، در این حالت از Single استفاده می‌شود. این متد یک مقدار را باز می‌گرداند و در صورتی که صفر یا بیش از یک مقدار در شرط صدق کند exception صادر می‌کند. متد SingleOrDefault رفتاری مشابه دارد اما اگر مقداری در شرط صدق نکند مقدار پیش فرض را باز می‌گرداند.
نکته: مقدار پیش فرض بستگی به نوع خروجی دارد که اگر object باشد مقدار null و اگر بطور مثال نوع عددی باشد، صفر می‌باشد.
private static void Query5()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.Single(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        try
        {
            var customer2 = context.Customers.Single(c => c.Name == "Mohsen");  // Exception: Sequence contains more than one element
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown");  // customer3 == null

        var customer4 = context.Customers.Single(c => c.Name == "Vahid");  // customer4 != null
    }
}

متد First:
در صورتیکه به اولین نتیجه پرس و جو نیاز هست می‌توان از First استفاده کرد. اگر پرس و جو نتیجه در بر نداشته باشد یعنی null باشد exception صادر خواهد شد اما اگر FirstOrDefault استفاده شود مقدار پیش فرض برگردانده خواهد شد.
private static void Query6()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.First(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown");  // customer2 == null
        var customer3 = context.Customers.First(c => c.Name == "Mohsen");
    }
}

اشتراک‌ها
گپ و گفتی با مهندسان طراح دات نت در مورد آینده این فریم ورک

This article dives into the mock questions I would ask, along with responses that are my personal best guess to the answers. Could my answers not reflect actual opinions shared by the team at Microsoft? Sure, but I'm hoping folks from the .NET team can jump in to correct me if I am way off base.

This is a rather interesting time for .NET – what's being done shapes the future of .NET for the next decade. Let's ask the honest questions and hopefully all of us will understand the new .NET ecosystem a little better. 

گپ و گفتی با مهندسان طراح دات نت در مورد آینده این فریم ورک
نظرات مطالب
Vue.js - نصب و راه‌اندازی اولیه - قسمت اول
نکته تکمیلی:
در ورژن جدید دستور
npm install -g vue-cli
 به 
npm install -g @vue/cli
تغییر نموده.
 The package name changed from  vue-cli  to  @vue/cli . 

بعد از نصب  cli با دستور 
vue create my-app
میتوان از طریق npm ساختار و تمپلیت‌های اولیه برنامه ای مبتنی بر Vue.js  را دریافت کرد.
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت پنجم
در این قسمت به بررسی بخش Collections (امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی‌های مختلف) ، بخش آگهی‌ها، سیستم لاگ عملیات کاربران و مدل‌های سیستمی می‌پردازیم.
در مدل‌های سیستم، یک تغییر کلی به منظور نگهداری آخرین تغییر دهنده و آخرین تاریخ تغییر در رکورد‌ها، ایجاد شده است. کلاس پایه‌ی زیر به منظور کپسوله کردن یکسری خصوصیات تکراری در نظر گرفته شده است.
  public abstract class BaseEntity
    {
        #region Properties
        /// <summary>
        /// gets or sets Identifier of this Entity
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date that this entity was created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets Date that this entity was updated
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// gets or sets TimeStamp for prevent concurrency Problems
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets ro sets User that Modify this entity
        /// </summary>
        public virtual User ModifiedBy { get; set; }
        /// <summary>
        /// gets ro sets Id of  User that modify this entity
        /// </summary>
        public virtual long? ModifiedById { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual User CreatedBy { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual long CreatedById { get; set; }
        #endregion
    }
با توجه به امکان تغییر نام کاربری توسط کاربر در سیستم، نگه داری صرفا نام کاربری آخرین تغییر دهنده، مفید نخواهد بود. شبیه به این کار را در سیستم Decision نیز می‌توانید مشاهده کنید. خصوصیاتی که نیاز به توضیح دارند :
  • ReportedOn : نگهداری آخرین تاریخ اخطار 
  • ModifyLocked : به منظور ممانعت از ویرایش 
  • CreatedBy,CreatedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان ایجاد کننده)
  • ModifiedBy , ModifiedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان آخرین تغییر دهنده)

مدل کلکسیون (کالکشن ،Collection)

بخش Collections می‌تواند برای کاربران امکان انتشار مطالب گروهبندی شده را بر اساس موضوع‌های مختلف، مهیا کند. کاربران بعد از کسب امتیازات لازم با استفاده از مهیا کردن محتوای وبلاگ یا فعالیت‌های آنها در انجمن و ... می‌توانند دسترسی لازم را برای ساخت Collections‌‌های خود، داشته باشند.  
 /// <summary>
    /// Represents the Collection for group posts by topic 
    /// </summary>
    public class Collection : GuidBaseEntity
    {
        #region Properties
        /// <summary>
        /// gets or sets name of collection
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// gets or sets Alternative SlugUrl
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets some description of group
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// gets or sets description that indicate how to pay 
        /// </summary>
        public virtual string HowToPay { get; set; }
        /// <summary>
        /// gets or sets Visibility Type 
        /// </summary>
        public virtual CollectionVisibility Visibility { get; set; }
        /// <summary>
        /// gets or sets color of Collection's Cover
        /// </summary>
        public virtual string Color { get; set; }
        /// <summary>
        /// gets or sets Name of Image that used as Cover
        /// </summary>
        public virtual string Photo { get; set; }
        /// <summary>
        /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// indicate this collection is active or not
        /// </summary>
        public virtual bool IsActive { get; set; }
        #endregion

        #region  NavigationProperties
       
        /// <summary>
        /// get or set collection of attachments that attached in this group
        /// </summary>
        public virtual ICollection<CollectionAttachment> Attachments { get; set; }
        /// <summary>
        /// get or set tags of collection
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        /// <summary>
        /// get or set List Of Posts that Associated with this Collection
        /// </summary>
        public virtual ICollection<CollectionPost> Posts { get; set; }
        /// <summary>
        /// get or set Users that they are Member of this collection if visibility is Custom
        /// </summary>
        public virtual ICollection<User> Memebers { get; set; }
        #endregion
    }
   public enum  CollectionVisibility
    {
        Friends,
        OnlyMe,
        Public,
        NotFree,
        Custom
    }
مدل بالا نشان دهنده‌ی کلکسیون‌های ایجاد شده‌ی توسط کاربران می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  • Visibility : برای اعمال محدودیت دسترسی به یک کلکسیون در نظر گرفته شده است. از نوع، نوع داده‌ی شمارشی CollectionVisibility پیاده سازی شده‌ی دربالا، می‌باشد. حالت Custom برای زمانی است که نیاز است صرفا یک سری از کاربران بدون هزینه‌، دسترسی برای مطالعه‌ی این کلکسیون داشته باشند. حالت NotFree هم برای زمانی است که کاربرانی که هزینه‌ی مورد نظر را پرداخت کرده باشند، به عنوان عضو به این کلکسیون می‌توانند دسترسی داشته باشند.
  • Members : به منظور اعمال ارتباط چند به چند بین مدل کاربر و مدل کلکسیون، در نظر گرفته شده است و زمانی این لیست اعضا خالی نیست که حالت Visibility با NotFree یا Custom مقدار دهی شده باشد.
  • Tags : برای یافتن راحت‌تر کلکسیون‌های مورد نظر کاربران، یک ارتباط چند به چند بین کلکسیون‌ها و مخزن برچسب مطرح شده‌ی در مقاله اول، در نظر گرفته شده است. 
  • TagNames : برای افزایش کارآیی سیستم در نظر گرفته شده است و نام برچسب‌های در ارتباط با کلکسیون را در خود به صورت جدا شده با (,) نگهداری می‌کند.
  • Posts : لیست پست‌هایی است که توسط مدیر کلکسیون در آن ارسال می‌شود. لذا یک ارتباط یک به چند بین کلکسیون‌ها و CollectionPost در نظر گرفته شده است.
  • Attachments : لیست فایل‌هایی است که در کلکسیون بارگذاری شده‌اند (در ادامه پیاده سازی خواهد شد).
  • Owner , OwnerId : هر کلکسیون نیز یک صاحب خواهد داشت. بدین منظور یک ارتباط یک به چند بین کاربر و کلکسیون برقرار شده است.
  • HowToPay : توضیحاتی در مورد نحوه‌ی پرداخت هزینه (در صورتی که Visibility این کلکسیون NotFree باشد).

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

 public class CollectionPost : GuidBaseEntity
    {
        #region Properties
      
        /// <summary>
        /// indicate this post should be pin
        /// </summary>
        public virtual bool IsPin { get; set; }
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugAltUrl { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed 
        /// </summary>
        public virtual bool AllowComments { get; set; }
        /// <summary>
        /// indicate comments should be approved before display
        /// </summary>
        public virtual bool ModerateComments { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  approved comments
        /// <remarks>The same as if we run Item.Comments.Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int ApprovedCommentsCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  unapproved comments
        /// <remarks>The same as if we run Item.Comments.Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int UnApprovedCommentsCount { get; set; }
        /// <summary>
        /// gets or sets rating complex instance
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// indicate users can share this post
        /// </summary>
        public virtual bool IsEnableForShare { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set comments of this post
        /// </summary>
        public virtual ICollection<CollectionComment> Comments { get; set; }
       
        /// <summary>
        /// gets or sets collection that associated with this post
        /// </summary>
        public virtual Collection Collection { get; set; }
        /// <summary>
        /// gets or sets id of collection that associated with this post
        /// </summary>
        public virtual Guid CollectionId { get; set; }
        #endregion
    }
مدل بالا نشان دهنده‌ی پست‌های ارسالی در کلکسیون‌ها می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  • IsEnableForShare : اگر با مقدار True مقدار دهی شده باشد ، امکان به اشتراک گذاری آن درشبکه‌های اجتماعی وجود خواهد داشت. 
  • Comments : اگر مقدار AllowComments مربوط به پست ارسالی true باشد، در آن صورت امکان نظر دهی به پست  هم امکان پذیر خواهد بود. برای برقراری ارتباط یک به چند بین مدل پست کلکسیون و نظرات کلکسیون، این لیست در مدل بالا گنجانده شده است.
  • ModerateComments : اگر برای پست خاصی، با مقدار true مقدار دهی شده باشد، نظرات آن پست قبل از نمایش باید توسط مدیر آن کلکسیون تأیید شوند.
  • Author , AuthorId : در واقع ارسال کننده‌ی تمام پست‌ها، همان صاحب کلکسیون می‌باشد. ولی برای راحتی واکشی لیست پست‌های ارسالی کاربر مورد نظر یک ارتباط یک به چند بین کاربر و پست‌های ارسالی را در کلکسیون اعمال کرده‌ایم.
  • Collection , CollectionId : کلید خارجی ما در دیتابیس ایجاد شده خواهد بود که نشان دهنده‌ی ارتباط یک به چند بین کلکسیون و پست‌ها می‌باشد.
  • IsPin : اگر لازم است پستی به عنوان اولین پست در کلکسیون نمایش داده شود، این خصوصیت برای آن true خواهد بود.
  • ApprovedCommentsCount , UnApprovedCommentsCount: برای افزایش کارآیی سیستم در نظر گرفته شده است و هنگام درج نظر جدید یا حذف نظر، ویرایش خواهند شد. 

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

 public class CollectionComment 
    {
        #region Ctor
        public CollectionComment()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            CreatedOn = DateTime.Now;

        }
        #endregion

        #region Properties
        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets date of creation 
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets informations of agent
        /// </summary>
        public virtual string UserAgent { get; set; }
        /// <summary>
        /// indicate this comment is Approved 
        /// </summary>
        public virtual bool IsApproved { get; set; }
        /// <summary>
        /// gets or sets Ip Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets counter for report this comment
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets post that this comment added it
        /// </summary>
        public virtual CollectionPost Post { get; set; }
        /// <summary>
        /// gets or sets id of post that this comment added it
        /// </summary>
        public virtual Guid PostId { get; set; }
        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// get or set Id of user that create this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        /// <summary>
        /// gets or sets CollectionComment's identifier for Replying and impelemention self referencing
        /// </summary>
        public virtual Guid? ReplyId { get; set; }
        /// <summary>
        /// gets or sets Collection's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual CollectionComment Reply { get; set; }
        /// <summary>
        /// get or set collection of Collection's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual ICollection<CollectionComment> Children { get; set; }
        #endregion
    }

مدل بالا نشان دهنده‌ی نظرات ارسالی برای پست‌های کلکسیون‌ها می‌باشد. صرفا کاربران عضو سیستم این اجازه را در صورتی خواهند داشت که برای پست مورد نظر خصوصیت AllowComments با مقدار true مقدار دهی شده باشد
حالت درختی آن مشخص است. برای اعمال ارتباط یک به چند بین پست‌ها و نظرات، از CollectionPost و CollectionPostId استفاده خواهد شد.

  • IsApproved : برای زمانی استفاده خواهد شد که خصوصیت ModerateComments پست مورد نظر با مقدار true مقدار دهی شده باشد. 
  • ReportsCount : به مانند بخش‌های قبل، تعداد اخطار‌های داده شده‌ی برای یک نظر را نشان خواهد داد. 
  • Creator,CreatorId : ارسال کننده‌ی نظر می‌باشد و برای ایجاد ارتباط یک به چند بین کاربر و نظرات کلکسیون‌ها در نظر گرفته شده‌اند. 
  • ReportedOn : نگه داری آخرین تاریخ اخطار 
  • ModifyLocked : به منظور ممانعت از ویرایش

مدل فایل‌های ضمیمه کلکسیون ها

public class CollectionAttachment : BaseAttachment
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets Collection  that this file attached 
        /// </summary>
        public virtual Collection Collection { get; set; }
        /// <summary>
        /// gets or sets Id of Collection  that this file attached
        /// </summary>
        public virtual Guid? CollectionId { get; set; }
        #endregion
    }
اگر یادتان باشد در مقاله‌ی دوم در مورد نحوه‌ی مدیریت فایل‌ها بحث شد و در نتیجه تصمیم گرفته شد که از ارث بری TPH استفاده کنیم. مدل بالا نیز یکی از SubClass‌های ما خواهد بود. با توجه به اینکه شاید Privacy فایل‌های ارسالی یک گروه مهم باشد (در صورت خصوصی بودن یا پولی بودن مطالعه‌ی کلکسیون)  نیاز شد مدل بالا را نیز داشته باشیم. برای اعمال ارتباط یک به چند بین مدل بالا و مدل کلکسیون، از خصوصیت‌های Colllection , CollectionId استفاده خواهیم کرد. دقت کنید که لازم است CollectionId به صورت نال پذیر درنظر گرفته شود.

مدل آگهی ها

/// <summary>
    /// Represents Announcement For Announcement Section
    /// </summary>
    public class Announcement : BaseContent
    {
        #region Properties
        /// <summary>
        /// gets or sets Date that this Announcement will Expire
        /// </summary>
        public virtual DateTime? ExpireOn { get; set; }
        /// <summary>
        /// indicate this accouncement is approved by admin if announcementSetting.Moderate==true
        /// </summary>
        public virtual bool IsApproved { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set Collection of Comments for this Announcement
        /// </summary>
        public virtual ICollection<AnnouncementComment> Comments { get; set; }

        #endregion
    }
مدل بالا نشان دهنده‌ی آگهی‌ها سیستم خواهد بود. همان طور که مشخص است، این مدل نیز از کلاس پایه BaseContent ارث بری کرده و علاوه بر آن یکسری خصوصیت دیگر را به شرح زیر دارد:
  • ExpireOn : زمان انقضای آگهی 
  • IsApproved : به منظور اعمال مدیریتی در نظر گرفته شده است
  • Comments : اگر امکان ارسال نظرات برای آگهی از بخش تنظیمات فعال باشد، این لیست نظرات ما را نگه داری خواهد کرد. لذا یک رابطه‌ی یک به چند بین نظرات و آگهی‌ها خواهد بود.

مدل نظرات آگهی ها

/// <summary>
    /// Repersents Comment For Announcement
    /// </summary>
    public class AnnouncementComment : BaseComment
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual AnnouncementComment Reply { get; set; }
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual ICollection<AnnouncementComment> Children { get; set; }
        /// <summary>
        /// gets or sets announcement that this comment sent to it
        /// </summary>
        public virtual Announcement Announcement { get; set; }
        /// <summary>
        /// gets or sets announcement'Id that this comment sent to it
        /// </summary>
        public virtual long AnnouncementId { get; set; }
        #endregion
    }
مدل بالا  نشان دهنده‌ی نظرات ارسالی برای آگهی‌ها می‌باشد. از کلاس پایه‌ی مطرح شده‌ی در مقاله‌ی اول ارث بری کرده و علاوه بر آن ساختار درختی آن مشخص است و همچنین برای برقراری ارتباط یک به چند با مدل آگهی‌ها، خصوصیات Announcement  , AnnouncementId در نظر گرفته شده‌اند.

مدل سیستم لاگ عملیات کاربران

/// <summary>
    /// Represent The Operation's log
    /// </summary>
    public class AuditLog
    {
        #region Ctor
        /// <summary>
        /// 
        /// </summary>
        public AuditLog()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            OperatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier of AuditLog
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// sets or gets description of Log
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// sets or gets when log is operated
        /// </summary>
        public virtual DateTime OperatedOn { get; set; }
        /// <summary>
        /// sets or gets log's section
        /// </summary>
        public virtual AuditSection Section { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets log's creator
        /// </summary>
        public virtual User OperatedBy { get; set; }
        /// <summary>
        /// sets or gets identifier of log's creator
        /// </summary>
        public virtual long OperatedById { get; set; }
        #endregion
    }

  public enum AuditSection
    {
        Blog,
        News,
        Forum,
        ...
    }
مدل بالا نگهدارنده زمان اکشن انجام شده، توسط کاربری که انجام شده و یه سری توضیحات کلی می‌باشد. به منظور اعمال ارتباط یک به چند مابین کاربر و مدل بالا، خصوصیات OperatedBy و OperatedById را در نظر گرفته‌ایم. خصوصیت Section که از نوع، نوع داده شمارشی AuditSection می‌باشد، می‌تواند برای جداسازی این لاگ‌های ذخیره شده، مفید باشد.

مدل دامین‌های ممنوع

در پنل مدیریت امکانی را خواهیم داشت تا یکسری از دامین‌های مد نظر را که نمی‌خواهیم در محتوای سیستم، آدرس صفحات آنها دیده شوند و همچنین برای عدم ارسال و دریافت پینگ بک‌ها لحاظ شوند، ثبت کنیم.
 /// <summary>
    /// Represents Domain that is banned
    /// </summary>
    public class BannedDomain
    {
        #region Propertie
        /// <summary>
        /// gets or sets identifier of Domain
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets DomainName
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime BannedOn { get; set; }
        #endregion
    }

مدل کلمات ممنوع 

 /// <summary>
    /// Represents the banned words
    /// </summary>
    public class BannedWord
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="BannedWord"/>
        /// </summary>
        public BannedWord()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of BannedWord
        /// </summary>
        public Guid Id { get; set; }
        /// <summary>
        /// gets or sets Bad word
        /// </summary>
        public string BadWord { get; set; }
        /// <summary>
        /// gets or sets Good replaceword
        /// </summary>
        public string GoodWord { get; set; }
        /// <summary>
        /// indicating that this Word is spam
        /// </summary>
        public bool IsStopWord { get; set; }
        #endregion

    }
مدل بالا نشان دهنده‌ی کلماتی است که لازم است در متن مطالب سیستم حذف یا با کلمات جدید جایگزین شوند.
  • BadWord : کلمه مورد نظر که قرار است Ban شود.
  • IsStopWord : اگر لازم نیست جایگزینی برای کلمه استفاده شود و فقط لازم است حذف گردد، مقدار این خصوصیت true خواهد بود.
  • GoodWord : کلمه جایگزین 

مدل تنظیمات سیستم

 /// <summary>
    /// Represent The CMS setting
    /// </summary>
    public class Setting
    {
        #region Properties
        /// <summary>
        /// sets or gets name of setting
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// sets or gets value of setting
        /// </summary>
        public virtual string Value { get; set; }
        /// <summary>
        /// sets or gets Type of setting
        /// </summary>
        public virtual string Type { get; set; }
        #endregion
    }
مدل بالا کل تنظیمات سیستم را در قالب Key , Value نگهداری خواهد کرد. برای توضیحات این قسمت می‌توانید به مقاله‌ی "ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF" رجوع کنید.
پ.ن:در مقاله‌ی بعد با پیاده سازی مدل‌های مدیریت کاربران، سیستم پیام رسانی، سیستم ترفیع رتبه و ارتباط دوستی، DomainClasses به اتمام خواهد رسید.

نتیجه‌ی این قسمت 

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

مطالب
پیاده سازی SoftDelete در EF Core
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کرده‌اید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامه‌های تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آن‌ها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» می‌کنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقه‌ی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را می‌دهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شده‌اند، اما هنوز در برنامه‌ی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کرده‌است. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف می‌شود.

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


نیاز به تعریف دو خاصیت جدید در هر جدول

هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. می‌توان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا می‌توان ابتدا یک کلاس پایه‌ی abstract را برای آن ایجاد کرد:
using System;

namespace EFCoreSoftDelete.Entities
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }


        public bool IsDeleted { set; get; }
        public DateTime? DeletedAt { set; get; }
    }
}
و سپس موجودیت‌هایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Blog : BaseEntity
    {
        public string Name { set; get; }

        public virtual ICollection<Post> Posts { set; get; }
    }

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired();
            builder.HasIndex(blog => blog.Name).IsUnique();

            builder.HasData(new Blog { Id = 1, Name = "Blog 1" });
            builder.HasData(new Blog { Id = 2, Name = "Blog 2" });
            builder.HasData(new Blog { Id = 3, Name = "Blog 3" });
        }
    }
}
که هر بلاگ از تعدادی مطلب تشکیل شده‌است:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Post : BaseEntity
    {
        public string Title { set; get; }

        public Blog Blog { set; get; }
        public int BlogId { set; get; }
    }

    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.Property(post => post.Title).HasMaxLength(450);
            builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId);

            builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" });
            builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" });
            builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" });
            builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" });
            builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" });
        }
    }
}
مزیت علامتگذاری این کلاس‌ها، امکان کوئری گرفتن از آن‌ها نیز می‌باشد که در ادامه از آن استفاده خواهیم کرد.


حذف خودکار رکوردهایی که Soft Delete شده‌اند، از نتیجه‌ی کوئری‌ها و گزارشات

تا اینجا فقط دو خاصیت ساده را به کلاس‌های مدنظر خود اضافه کرده‌ایم. پس از آن یا می‌توان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائه‌ی EF Core 2x می‌توان برای آن‌ها Query Filter تعریف کرد. برای مثال می‌توان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئری‌های مرتبط با این موجودیت‌ها اعمال کرد:
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            // ...
            builder.HasQueryFilter(blog => !blog.IsDeleted);
        }
    }
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را می‌دهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئری‌های نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[IsDeleted] <> CAST(1 AS bit)


اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیت‌ها

همانطور که عنوان شد، مزیت علامتگذاری موجودیت‌ها با کلاس پایه‌ی BaseEntity، امکان کوئری گرفتن از آن‌ها است:
namespace EFCoreSoftDelete.DataLayer
{
    public static class GlobalFiltersManager
    {
        public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model
                                                    .GetEntityTypes()
                                                    .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType)))
            {
                entityType.addSoftDeleteQueryFilter();
            }
        }

        private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(GlobalFiltersManager)
                                .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
                                .MakeGenericMethod(entityData.ClrType);
            var filter = methodToCall.Invoke(null, new object[] { });
            entityData.SetQueryFilter((LambdaExpression)filter);
        }

        private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity
        {
            return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted);
        }
    }
}
در اینجا در ابتدا تمام موجودیت‌هایی که از BaseEntity ارث بری کرده‌اند، یافت می‌شوند. سپس بر روی آن‌ها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول می‌کند که نمونه‌ی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال می‌شود.

محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیت‌های موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {

        //...


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

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly);
            modelBuilder.ApplySoftDeleteQueryFilters();
        }


مشکل! هنوز هم حذف فیزیکی رخ می‌دهد!

تنظیمات فوق، تنها بر روی کوئری‌های نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ می‌دهد.
 برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آن‌را تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer
{
    public static class AuditableEntitiesManager
    {
        public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context)
        {
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //TODO: ...
                        break;
                    case EntityState.Modified:
                        //TODO: ...
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method.

                        entry.Entity.IsDeleted = true;
                        entry.Entity.DeletedAt = now;
                        break;
                }
            }
        }
    }
}
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شده‌ی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا می‌کنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش می‌شوند، وضعیت جدید Modified خواهد بود که به کوئری‌های Update تفسیر می‌شوند. به این ترتیب می‌توان همانند قبل یک رکورد را حذف کرد:
var post1 = context.Posts.Find(1);
if (post1 != null)
{
   context.Remove(post1);

   context.SaveChanges();
}
اما دستوری که توسط EF Core صادر می‌شود، یک Update است:
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            ChangeTracker.DetectChanges();

            beforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges(acceptAllChangesOnSuccess);

            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        // ...

        private void beforeSaveTriggers()
        {
            setAuditProperties();
        }

        private void setAuditProperties()
        {
            this.SetAuditableEntityOnBeforeSaveChanges();
        }
    }
}


مشکل! رکوردهای وابسته حذف نمی‌شوند!

حالت پیش‌فرض حذف رکوردها در EFCore به cascade delete تنظیم شده‌است. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابسته‌ی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
 ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1);
if (blog1 != null)
{
   context.Remove(blog1);

   context.SaveChanges();
}
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
var blog1AndItsRelatedPosts = context.Blogs
    .Include(blog => blog.Posts)
    .FirstOrDefault(blog => blog.Id == 1);
if (blog1AndItsRelatedPosts != null)
{
    context.Remove(blog1AndItsRelatedPosts);

    context.SaveChanges();
}
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید می‌کند:
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title]
FROM (
SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1)
) AS [t]
LEFT JOIN (
SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[IsDeleted] <> CAST(1 AS bit)
) AS [t0] ON [t].[Id] = [t0].[BlogId]
ORDER BY [t].[Id], [t0].[Id]

Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True',
 @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3',
@p6='2020-09-17T05:25:00' (Nullable = true), @p7='True',
 @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7
WHERE [Id] = @p8;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10
WHERE [Id] = @p11;
SELECT @@ROWCOUNT;
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آن‌را که پیشتر حذف منطقی نشده‌اند، یکی یکی به صورت حذف شده، علامتگذاری می‌کند. به این ترتیب cascade delete منطقی نیز در اینجا میسر می‌شود.


یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد

فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کرده‌اید و اکنون رکوردی در این بین، حذف منطقی شده‌است. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شده‌است؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده می‌کنید، از قابلیتی به نام filtered indexes پشتیبانی می‌کند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکس‌ها وجود دارد. در این حالت می‌توان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreSoftDelete.zip
مطالب
ایجاد پروژه‌ی «کتابخانه» توسط Angular CLI 6.0
یکی از مواردی که با Angular CLI 6.0 به شدت ساده شده‌است، ایجاد پروژه‌های «کتابخانه» Angular است. برای مثال شاید در حین استفاده‌ی از بعضی از کتابخانه‌ی ثالث تهیه شده‌ی برای Angular با خطای ذیل مواجه شده باشید:
Please open an issue in the library repository to alert its author and ask them to 
package the library using the Angular Package Format (https://goo.gl/jB3GVv).
این خطا زمانی رخ می‌دهد که تهیه کننده‌ی کتابخانه، فرمت بسته‌های Angular را رعایت نکرده باشد و ... رعایت کردن آن نیز کار بسیار مشکلی است. نگارش 6 در پشت صحنه، پروژه‌ی موفق ng-packagr را به مجموعه‌ی CLI اضافه کرده‌است و از این پس توسط خود CLI می‌توان کتابخانه‌های استاندارد Angular را تولید کرد. این مورد، مزیت استاندارد سازی کتابخانه‌ها‌ی npm حاصل را نیز به همراه دارد. مشکلی که گاهی از اوقات به علت عدم رعایت این ساختار با بسته‌های فعلی npm مخصوص Angular وجود دارند؛ مانند خطایی که عنوان شد. برای مثال بدون استفاده‌ی از این ابزار، نیاز است مستندات چند صفحه‌ای ساخت کتابخانه‌های Angular را سطر به سطر پیاده سازی کنید که توسط CLI 6.0 به صورت خودکار ایجاد و مدیریت می‌شود.


مراحل ایجاد یک پروژه‌ی «کتابخانه» توسط Angular CLI 6.0

مرحله‌ی اول ایجاد یک پروژه‌ی کتابخانه، مانند قبل، توسط دستور ng new و ایجاد یک پروژه‌ی دلخواه جدید است:
 ng new my-lib-test
به همراه Angular CLI 6.0، فرمت تنظیمات آن نیز تغییر کرده‌است و مفهوم workspace به آن اضافه شده‌است که در آن می‌توان چندین پروژه را تعریف کرد.
پس از ایجاد پروژه‌ی my-lib-test توسط دستور فوق و وارد شدن به پوشه‌ی اصلی آن توسط خط فرمان، می‌توان با اجرای دستور زیر، پروژه‌های دیگری را به پروژه‌ی جاری افزود:
 ng generate application my-app-name
اما اگر در اینجا بجای ذکر application، از نام library استفاده کنیم، یک کتابخانه را بجای یک برنامه، به workspace جاری اضافه می‌کند:
 ng generate library my-lib
پس از اجرای این دستور اگر به فایل angular.json دقت کنیم، این پروژه در ذیل projects اضافه شده‌است:


همچنین یک پوشه‌ی جدید به نام projects نیز ایجاد شده و پروژه‌ی my-lib داخل آن قرار گرفته‌است.


فایل جدید public_api.ts

پس از ایجاد کتابخانه‌ی جدید «my-lib»، فایل جدیدی به نام projects\my-lib\src\public_api.ts نیز به آن اضافه شده‌است:


با این محتوا:
/*
* Public API Surface of my-lib
*/
export * from './lib/my-lib.service';
export * from './lib/my-lib.component';
export * from './lib/my-lib.module';
هر خروجی که در اینجا ذکر شود توسط استفاده کنندگان از این کتابخانه قابل دسترسی خواهد بود. برای مثال دستور «ng generate library my-lib» مطابق تصویر فوق، یک سرویس جدید را به نام my-lib.service، یک کامپوننت جدید را به نام my-lib.component و یک ماژول جدید را به نام my-lib.module به صورت پیش‌فرض ایجاد کرده و درون پوشه‌ی lib قرار داده‌است. اکنون آن‌ها را توسط فایل public_api.ts، به نحوی که مشاهده می‌کنید در معرض دید استفاده کنندگان قرار می‌دهد.
برای مثال اگر فایل جدید projects\my-lib\src\lib\my-lib.models.ts را به این کتابخانه اضافه کنیم که شامل تعدادی مدل و اینترفیس قابل دسترسی توسط استفاده کنندگان باشد، باید یک سطر زیر را به انتهای فایل public_api.ts اضافه کنیم:
 export * from './lib/my-lib.models';

این پروژه‌ی کتابخانه حتی به همراه فایل‌های package.json, tsconfig.json, tslint.json مخصوص به خود نیز می‌باشد تا بتوان آن‌ها را صرفا جهت این پروژه سفارشی سازی کرد.


ساختار my-lib.service پیش‌فرض یک پروژه‌ی کتابخانه

اگر به فایل projects\my-lib\src\lib\my-lib.service.ts دقت کنیم:
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MyLibService {

  constructor() { }
}
تمام قسمت‌های آن مانند قبل است، منهای 'providedIn: 'root آن. این مورد تنظیم جدیدی است که در پروژه‌های Angular 6 قابل استفاده‌است. هدف از آن، ارائه‌ی یک سرویس، بدون نیاز به ثبت صریح آن در قسمت providers یک NgModule است.
شاید بپرسید چرا؟ هدف اصلی از آن، بهبود فرآیند tree-shaking یا حذف کدهای مرده و استفاده نشده‌است. ممکن است سرویسی را تعریف کنید، اما در برنامه استفاده نشود. این حالت خصوصا در پروژه‌های کتابخانه‌های ثالث ممکن است زیاد رخ دهد. به همین جهت با ارائه‌ی این قابلیت، امکان حذف ساده‌تر سرویس‌هایی که در برنامه استفاده نشده‌اند از خروجی نهایی کامپایل شده، وجود خواهد داشت.


چگونه به پروژه‌ی کتابخانه‌ی جدید، یک کامپوننت جدید را اضافه کنیم؟

تمام دستورات Angular CLI، در اینجا نیز کار می‌کنند. تنها تفاوت آن‌ها، ذکر صریح نام پروژه‌ی مورد استفاده است:
 ng generate component show-data --project=my-lib
دستور فوق کامپوننت جدید show-data را به پروژه‌ی my-lib اضافه خواهد کرد؛ به همراه به روز رسانی خودکار فایل projects/my-lib/src/lib/my-lib.module.ts این پروژه، جهت ثبت کامپوننت اضافه شده.
البته در اینجا باید فایل my-lib.module.ts را اندکی ویرایش کرد و ShowDataComponent را به قسمت exports نیز افزود:
@NgModule({
  imports: [
    CommonModule,
    HttpClientModule
  ],
  declarations: [MyLibComponent, ShowDataComponent],
  exports: [MyLibComponent, ShowDataComponent]
})
export class MyLibModule { }
به صورت پیش‌فرض، کامپوننت جدید را در قسمت declarations معرفی می‌کند. یک چنین کامپوننتی فقط داخل همان lib قابل استفاده‌است. اگر قرار است خارج از این lib نیز به آن دسترسی داشته باشیم، باید آن‌را در قسمت exports نیز قید کنیم.
همچنین قسمت imports آن نیز به صورت پیش‌فرض خالی است. اگر نیاز است با ngIf کار کنید، باید CommonModule را در اینجا قید کنید و اگر نیاز است تبادلات HTTP وجود داشته باشد، ذکر HttpClientModule نیز ضروری است.


مرحله‌ی ساخت پروژه

پیش از استفاده‌ی از این پروژه‌ی کتابخانه، باید آن‌را build کرد:
 ng build my-lib
در اینجا نیز دستور ng build مانند قبل است، با این تفاوت که نام پروژه‌ی کتابخانه نیز در اینجا ذکر شده‌است.
پس از اجرای این دستور، خروجی ذیل مشاهده می‌شود:
Building Angular Package
Building entry point 'my-lib'
Rendering Stylesheets
Rendering Templates
Compiling TypeScript sources through ngc
Downleveling ESM2015 sources through tsc
Bundling to FESM2015
Bundling to FESM5
Bundling to UMD
Minifying UMD bundle
Remap source maps
Relocating source maps
Copying declaration files
Writing package metadata
Removing scripts section in package.json as it's considered a potential security vulnerability.
Built my-lib
Built Angular Package!
- from: D:\my-lib-test\projects\my-lib
- to: D:\my-lib-test\dist\my-lib
همانطور که ملاحظه می‌کنید، پس از طی مراحل خاص تولید یک کتابخانه، خروجی نهایی آن‌را در پوشه‌ی dist\my-lib قرار داده‌است.


استفاده‌ی از کتابخانه‌ی تولید شده

پس از پایان موفقیت آمیز مرحله‌ی Build، اکنون نوبت به استفاده‌ی از این کتابخانه است. استفاده‌ی از آن نیز همانند تمام کتابخانه‌ها و وابستگی‌های ثالثی است که تا پیش از این از آن‌ها استفاده کرده‌ایم. برای مثال ماژول آن‌را در قسمت imports مربوط به NgModule کلاس AppModule معرفی می‌کنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و MyLibModule را به نحو ذیل اضافه می‌کنیم:
import { MyLibModule } from "my-lib";

@NgModule({
  imports: [
    BrowserModule,
    MyLibModule
  ]
})
export class AppModule { }
نکته‌ی مهمی که در اینجا باید به آن دقت داشت این است که هرچند در این پروژه، MyLibModule داخل پوشه‌ی projects\my-lib\src\lib قرار دارد، اما نباید مسیر نسبی آن‌را در اینجا ذکر کرد و باید صرفا نام پوشه‌ی my-lib واقع در پوشه‌ی node_modules را در اینجا در حین مسیر دهی import آن معرفی کرد (همانند تمام وابستگی‌های ثالث دیگر).
اما سؤال اینجا است که آیا این پوشه پس از build، داخل پوشه‌ی node_modules نیز کپی شده‌است؟ پاسخ آن خیر است و برای مدیریت خودکار آن، به صورت زیر عمل شده‌است:
اگر به فایل tsconfig.json اصلی و واقع در ریشه‌ی workspace دقت کنید، پس از اجرای دستور «ng generate library my-lib»، قسمت paths آن نیز به صورت خودکار ویرایش شده‌است:
{
  "compilerOptions": {
    "paths": {
      "my-lib": [
        "dist/my-lib"
      ]
    }
  }
}
معنای آن این است که هرگاه import ایی در برنامه به my-lib اشاره کند، کامپایلر TypeScript می‌داند که باید آن‌را از پوشه‌ی dist/my-lib دریافت و پردازش کند. به همین جهت در اینجا دیگر نیازی به کپی دستی این پوشه، به پوشه‌ی node_modules وجود ندارد.

برای نمونه اگر شاره‌گر ماوس را بر روی my-lib قرار دهید، به درستی مسیر خوانده شدن آن، تشخیص داده می‌شود.

به این ترتیب مسیر این import‌، چه در این پروژه‌ی محلی و چه برای کسانیکه پوشه‌ی dist/my-lib را به صورت یک بسته‌ی npm جدید دریافت کرده‌اند، یکی خواهد بود.

در ادامه اگر به فایل app.component.html مراجعه کرده و selector کامپوننت show-data را به آن اضافه کنیم:
 <lib-show-data></lib-show-data>
می‌توان محتویات این کامپوننت دریافت شده‌ی از کتابخانه را مشاهده کرد.


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

برای اینکه این کتابخانه‌ی تولیدی را در اختیار عموم، در سایت npm قرار دهیم، ابتدا باید کتابخانه را در حالت production build تولید و سپس آن‌را publish کرد:
ng build my-lib --prod
cd dist/my-lib
npm publish
سطر اول، کتابخانه‌ی my-lib را در حالت production تواید می‌کند. سپس به پوشه‌ی فایل‌های نهایی تولید شده وارد می‌شویم و دستور npm publish را صادر می‌کنیم.
البته دستور آخر نیاز به ایجاد یک اکانت در سایت npm و وارد شدن به آن‌را دارد. جزئیات بیشتر آن در اینجا.