مطالب
رمزنگاری خودکار فیلدها توسط Entity Framework Core
از EF Core 2.1 به بعد، قابلیت جدیدی تحت عنوان «تبدیلگرهای مقدار»، به آن اضافه شده‌است. برای مثال در EF Core، زمانیکه اطلاعات Enums، در بانک اطلاعاتی ذخیره می‌شوند، معادل عددی آن‌ها درج خواهند شد. اگر علاقمند باشید تا بجای این مقادیر عددی دقیقا همان رشته‌ی تعریف کننده‌ی Enum درج شود، می‌توان یک «تبدیلگر مقدار» را برای آن نوشت. برای مثال در موجودیت Rider زیر، خاصیت Mount از نوع یک enum است.
public class Rider
{
    public int Id { get; set; }
    public EquineBeast Mount { get; set; }
}

public enum EquineBeast
{
    Donkey,
    Mule,
    Horse,
    Unicorn
}
برای اینکه در حین درج رکوردهای Rider در بانک اطلاعاتی دقیقا از مقادیر رشته‌ای EquineBeast استفاده شود، می‌توان به صورت زیر عمل کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
در اینجا در حین تعریف جزئیات نگاشت یک مدل می‌توان متد جدید HasConversion را نیز فراخوانی کرد. پارامتر اول آن، روش تبدیل مقدار enum را به یک رشته، جهت درج در بانک اطلاعاتی و پارامتر دوم آن، روش تبدیل مقدار رشته‌ای خوانده شده‌ی از بانک اطلاعاتی را جهت وهله سازی یک Rider داری خاصیت enum، مشخص می‌کند.

نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمی‌شوند. اینکار پیاده سازی آن‌ها را ساده‌تر می‌کند و همچنین می‌توان آن‌ها را بین خواص نال‌پذیر و نال‌نپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمی‌توان تبدیلگر نوشت.

نکته 2: کاری که در متد HasConversion فوق انجام شده‌است، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. می‌توان اینکار را به صورت صریح نیز انجام داد:
var converter = new ValueConverter<EquineBeast, string>(
    v => v.ToString(),
    v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion(converter);
مزیت اینکار این است که اگر قرار شد برای چندین خاصیت از تبدیلگر مقدار مشابهی استفاده کنیم، می‌توان از یک converter تعریف شده بجای تکرار کدهای آن استفاده کرد.


تبدیلگرهای مقدار توکار EF Core

برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شده‌اند که به این شرح می‌باشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایه‌ای از بایت‌ها به یک رشته‌ی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایه‌ای از بایت‌ها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایه‌ای از بایت‌ها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایه‌ای از بایت‌ها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایه‌ای از بایت‌های UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن

برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و می‌توان به صورت زیر از آن استفاده کرد:
var converter = new EnumToStringConverter<EquineBeast>();
modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion(converter);
نکته: تمام تبدیل کننده‌های مقدار توکار EF Core، بدون حالت هستند. بنابراین می‌توان یک تک وهله‌ی از آن‌ها را بین چندین خاصیت به اشتراک گذاشت.


تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف

برای حالاتی که تبدیلگر مقدار توکاری تعریف شده‌است، صرفا تعریف نوع تبدیل، کفایت می‌کند:
modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion<string>();
برای نمونه در اینجا با ذکر نوع رشته، تبدیل enum به string به صورت خودکار انجام خواهد شد. معادل اینکار، تعریف نوع سمت بانک اطلاعاتی این خاصیت است:
public class Rider
{
    public int Id { get; set; }

    [Column(TypeName = "nvarchar(24)")]
    public EquineBeast Mount { get; set; }
}
در این حالت حتی نیازی به تعریف HasConversion هم نیست.


نوشتن تبدیلگر خودکار مقادیر خواص، به نمونه‌ای رمزنگاری شده

پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون می‌توانیم یک نمونه‌ی سفارشی از آن‌را نیز طراحی کنیم:
namespace DbConfig.Web.DataLayer.Context
{
    public class MyAppContext : DbContext
    {
      // …

        protected override void OnModelCreating(ModelBuilder builder)
        {
            var encryptedConverter = new ValueConverter<string, string>(
               convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt
               convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt
            );

            // Custom application mappings
            builder.Entity<ConfigurationValue>(entity =>
            {
                entity.Property(e => e.Value).IsRequired().HasConversion(encryptedConverter);
            });
        }
    }
}
در اینجا معکوس کردن رشته‌ها به عنوان الگوریتم ساده‌ی رمزنگاری اطلاعات انتخاب شده‌است. نحوه‌ی اعمال این ValueConverter جدید را نیز ملاحظه می‌کنید.
می‌توان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه می‌کنیم:
using System;

namespace Test
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public sealed class EncryptedAttribute : Attribute
    { }
}
هدف از این Attribute خالی، صرفا نشانه گذاری خاصیت‌هایی است که قرار است به صورت رمزنگاری شده در بانک اطلاعاتی ذخیره شوند؛ مانند خاصیت Value زیر:
namespace DbConfig.Web.DomainClasses
{
    public class ConfigurationValue
    {
        public int Id { get; set; }
        public string Key { get; set; }

        [Encrypted]
        public string Value { get; set; }
    }
}
پس از آن، متد OnModelCreating را به صورت زیر اصلاح می‌کنیم تا به کمک Reflection و اطلاعات موجودیت‌های ثبت شده‌ی در سیستم، متد SetValueConverter را بر روی خواصی که دارای EncryptedAttribute هستند، به صورت خودکار فراخوانی کند:
namespace DbConfig.Web.DataLayer.Context
{
    public class MyAppContext : DbContext
    {
        protected override void OnModelCreating(ModelBuilder builder)
        {
            var encryptedConverter = new ValueConverter<string, string>(
               convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt
               convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt
            );

            foreach (var entityType in builder.Model.GetEntityTypes())
            {
                foreach (var property in entityType.GetProperties())
                {
                    var attributes = property.PropertyInfo.GetCustomAttributes(typeof(EncryptedAttribute), false);
                    if (attributes.Any())
                    {
                        property.SetValueConverter(encryptedConverter);
                    }
                }
            }
        }


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

از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آن‌ها کاملا از دیدگاه سایر قسمت‌های برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کرده‌ایم (مقدار دهی متداول)، اما همانطور که ملاحظه می‌کنید، نمونه‌ی رمزنگاری شده‌ی آن به صورت خودکار در بانک اطلاعاتی درج شده‌است (پارامتر p0):
 Executed DbCommand (22ms) 
   [Parameters=[@p1='1', 
                @p0='.tset ot eulav wen A' (Nullable = false) (Size = 4000)],
CommandType='Text', CommandTimeout='180']
SET NOCOUNT ON;
UPDATE [Configurations] SET [Value] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;

و یا کوئری زیر
 db.Set<ConfigurationValue>().Where(x => x.Value.EndsWith("world!"))
به این نحو ترجمه خواهد شد:
SELECT [x].[Id], [x].[Key], [x].[Value]
FROM [Configurations] AS [x]
WHERE RIGHT([x].[Value], LEN(N'world!')) = N'!dlrow'
یعنی نیازی نیست تا مقداری را که در حال جستجوی آن هستیم، خودمان به صورت دستی رمزنگاری کرده و سپس در کوئری قرار دهیم. اینکار به صورت خودکار انجام می‌شود.
مطالب
یک دست سازی ی و ک در برنامه‌های Entity framework 6
تا قبل از EF 6 برای طراحی یک سیستم عمومی تغییر مقادیر ثبت شده در بانک اطلاعاتی، می‌شد با استفاده از امکانات توکار Tracking آن، مقادیر تغییر کرده را یافت و برای مثال ی و ک آن‌ها را پیش از درج در بانک اطلاعاتی، یک دست کرد. در EF 6 با معرفی یک سری interceptor می‌توان به مراحل پیش و پس از اجرای کوئری‌ها دسترسی پیدا کرد. عمده‌ترین کاربرد آن، لاگ کردن SQLهای تولیدی و نوشتن برنامه‌هایی شبیه به EF Profiler است. اما ... استفاده‌ی دیگری را نیز می‌توان از IDbCommandInterceptor جدید آن تدارک دید: دستکاری SQL تولیدی توسط آن پیش از اعمال به بانک اطلاعاتی.

طراحی یک Interceptor برای یک دست سازی ی و ک

در اینجا کدهای کلاس YeKeInterceptor را ملاحظه می‌کنید. در متدهایی که به کلمه‌ی Executing ختم می‌شوند، می‌توان به دستورات SQL تولید شده توسط EF، پیش از اعمال بر روی بانک اطلاعاتی دسترسی داشت:
    public class YeKeInterceptor : IDbCommandInterceptor
    {
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            command.ApplyCorrectYeKe();
        }

        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            command.ApplyCorrectYeKe();
        }

        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            command.ApplyCorrectYeKe();
        }
    }
DbCommand، حاوی تمام اطلاعاتی است که به آن نیاز داریم؛ شامل CommandText یا همان SQL تولید شده و همچنین command.Parameters برای دسترسی به مقادیر پارامترهای کوئری. نکته‌ی مهم تمام این موارد، قابل ویرایش بودن آن‌ها است.
    public static class YeKe
    {
        public const char ArabicYeChar = (char)1610;
        public const char PersianYeChar = (char)1740;

        public const char ArabicKeChar = (char)1603;
        public const char PersianKeChar = (char)1705;

        public static string ApplyCorrectYeKe(this object data)
        {
            return data == null ? null : ApplyCorrectYeKe(data.ToString());
        }

        public static string ApplyCorrectYeKe(this string data)
        {
            return string.IsNullOrWhiteSpace(data) ?
                        string.Empty :
                        data.Replace(ArabicYeChar, PersianYeChar).Replace(ArabicKeChar, PersianKeChar).Trim();
        }

        public static void ApplyCorrectYeKe(this DbCommand command)
        {
            command.CommandText = command.CommandText.ApplyCorrectYeKe();

            foreach (DbParameter parameter in command.Parameters)
            {
                switch (parameter.DbType)
                {
                    case DbType.AnsiString:
                    case DbType.AnsiStringFixedLength:
                    case DbType.String:
                    case DbType.StringFixedLength:
                    case DbType.Xml:
                        parameter.Value =   parameter.Value is DBNull ? parameter.Value : parameter.Value.ApplyCorrectYeKe();
                        break;
                }
            }
        }
    }
در اینجا پیاده سازی متد الحاقی ApplyCorrectYeKe را که در کلاس YeKeInterceptor مورد استفاده قرار گرفت، ملاحظه می‌کنید.
در آن، CommandText و همچنین parameter.Valueها در صورت رشته‌ای بودن، اصلاح می‌شوند.
سربار این روش نسبت به روش‌های پیشین استفاده از Reflection کمتر است. همچنین اشیاء پیچیده و تو در تو را نیز بهتر پشتیبانی می‌کند؛ چون در مرحله Executing، کار پردازش این اشیاء پایان یافته و SQL خام نهایی آن در اختیار ما است.


نحوه‌ی استفاده از YeKeInterceptor

در آغاز برنامه (برای مثال متد Application_Start فایل Global.asax.cs برنامه‌های MVC )، سطر زیر را فراخوانی کنید:
 DbInterception.Add(new YeKeInterceptor());

یک مثال کامل برای دریافت
Sample32.cs
نظرات مطالب
ایجاد یک Repository در پروژه برای دستورات EF
IQueryable فقط یک expression است. هنوز اجرا نشده. (expose آن از طریق وب سرویس اشتباه است و به مشکلات serialization برخواهید خورد.)
زمانیکه ToList، First و امثال آن روی این عبارت فراخوانی شود تبدیل به SQL شده و سپس بر روی بانک اطلاعاتی اجرا می‌شود. به این deferred execution یا اجرای به تعویق افتاده گفته می‌شود.
اگر این عبارت را در اختیار لایه‌های دیگر قرار دهید، یعنی انتهای کار را بازگذاشته‌اید و حد و حدود سیستم شما مشخص نیست. شما اگر IQueryable بازگشت دهید، در لایه‌ای دیگر می‌شود یک join روی آن نوشت و اطلاعات چندین جدول دیگر را استخراج کرد؛ درحالیکه نام متد شما GetUsers بوده. بنابراین بهتر است به صورت صریح اطلاعات را به شکل List بازگشت دهید، تا انتهای کار باز نمانده و طراحی شما نشتی نداشته باشد.
مطالب
حذف جوین‌های اضافی به یک جدول در Entity framework
تذکر: این مطلب و نکته برای تا EF 6.1.3 تهیه شده‌است و ممکن است در نگارش‌های آتی آن وجود نداشته یا برطرف شده‌باشد.

کوئری ذیل را در نظر بگیرید:
var productsList1 = ctx.Products.Where(product => product.Id > 1)
    .Include(product => product.Category)
    .Include(product => product.User)
    .Where(
        product =>
            product.Category.Title.Contains("t") && product.Category.Id > 1 && product.Price > 100)
    .OrderBy(product => product.Price)
    .ToList();
به نظر شما این کوئری چند Join را ایجاد می‌کند؟
احتمالا شاید عنوان کنید که به ازای هر Include یک join خواهیم داشت. بنابراین دو جوین به جداول کاربران و گروه‌های محصول‌ها ایجاد می‌شود.

اما ... در واقعیت این کوئری را تولید می‌کند:
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Price] AS [Price],
    [Extent1].[CategoryId] AS [CategoryId],
    [Extent1].[UserId] AS [UserId],
    [Extent3].[Id] AS [Id1],
    [Extent3].[Name] AS [Name1],
    [Extent3].[Title] AS [Title],
    [Extent3].[UserId] AS [UserId1],
    [Extent4].[Id] AS [Id2],
    [Extent4].[Name] AS [Name2]
    FROM    [dbo].[Products] AS [Extent1]
    INNER JOIN [dbo].[Categories] AS [Extent2] ON [Extent1].[CategoryId] = [Extent2].[Id]
    LEFT OUTER JOIN [dbo].[Categories] AS [Extent3] ON [Extent1].[CategoryId] = [Extent3].[Id]
    LEFT OUTER JOIN [dbo].[Users] AS [Extent4] ON [Extent1].[UserId] = [Extent4].[Id]
    WHERE ([Extent1].[Id] > 1) AND ([Extent2].[Title] LIKE N'%t%') AND ([Extent1].[CategoryId] > 1) AND ([Extent1].[Price] > 100)
    ORDER BY [Extent1].[Price] ASC
اگر به قسمت جوین‌های آن دقت کنید دوبار جوین به جدول Categories را می‌توانید مشاهده کنید.
این دو جوین حاصل یکبار Include جدول Categories و یکبار استفاده از navigation property آن در قسمت where است.

این باگ در اینجا گزارش شده، ولی به نظر هنوز برطرف نشده‌است یا مجددا ظاهر شده‌است.

برای رفع آن در حال حاضر بهترین راه حل استفاده از روش ذیل است:
var query2 = from product in ctx.Products
             let category = product.Category
             where product.Id > 1
             where category.Title.Contains("t") && category.Id > 1 && product.Price > 100
             select new { product, category }; 
var productsList2 = query2.ToList();
در اینجا قسمت Include کلا حذف شده و همچنین گروه‌ها توسط یک متغیر موقتی که با let ایجاد شده‌است، استفاده می‌شود. خروجی آن کوئری ذیل است:
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Price] AS [Price],
    [Extent1].[CategoryId] AS [CategoryId],
    [Extent1].[UserId] AS [UserId],
    [Extent2].[Id] AS [Id1],
    [Extent2].[Name] AS [Name1],
    [Extent2].[Title] AS [Title],
    [Extent2].[UserId] AS [UserId1]
    FROM  [dbo].[Products] AS [Extent1]
    INNER JOIN [dbo].[Categories] AS [Extent2] ON [Extent1].[CategoryId] = [Extent2].[Id]
    WHERE ([Extent1].[Id] > 1) AND ([Extent2].[Title] LIKE N'%t%') AND ([Extent2].[Id] > 1) AND ([Extent1].[Price] > 100)
همانطور که مشاهده می‌کنید، اینبار فقط یکبار جوین به جدول گروه‌ها تشکیل شده‌است.

چند نکته:
-در کوئری let دار، اگر در قسمت select نهایی فقط product ذکر شود، هرچند جوین به جدول گروه‌ها تشکیل می‌شود اما فیلدهای این جدول انتخاب نخواهند شد.
-معادل کوئری LINQ نوشته شده را اگر بخواهیم توسط متدهای الحاقی بازنویسی کنیم، به کوئری ذیل خواهیم رسید:
var query2ChainedVersion = ctx.Products
     .Select(product => new { product, category = product.Category })
     .Where(@t => @t.product.Id > 1)
     .Where(@t => @t.category.Title.Contains("t") && @t.category.Id > 1 && @t.product.Price > 100)
     .Select(@t => new { @t.product , @t.category });

اگر علاقمند به آزمایش این باگ هستید، کدهای کامل آن‌را از اینجا می‌توانید دریافت کنید:
  Sample38.zip
مطالب
اتصال Node.js به SQL Server با استفاده از Edge.js
اگر خواسته باشید که با استفاده از Node.js به SQL Server متصل شوید، احتمالا متوجه شده‌اید ماژولی که مایکروسافت منتشر کرده است، ناقص بوده و به صورت پیش نمایش است که بسیاری از ویژگی‌ها و مسائل مهم، در آن در نظر گرفته نشده است.

یکی دیگر از ماژول‌هایی که امکان اتصال Node.js را به SQL Server ممکن می‌کند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم می‌کند. این مسئله، توسعه دهندگان Node.js را قادر می‌سازد تا از فناوری‌هایی که به صورت سنتی استفاده‌ی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
  • SQL Server
  • Active Directory
  • Nuget packages
  • استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)


نصب Node.js

اگر Node.js را بر روی سیستم خود نصب ندارید، می‌توانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:

node -v
شما باید نسخه‌ی نصب شده‌ی Node.js را مشاهده کنید.

ایجاد پوشه پروژه

سپس پوشه‌ای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:

md \projects\node-edge-test1
cd \projects\node-edge-test1

نصب Edge.js

Node با استفاده از package manager خود دانلود و نصب ماژول‌ها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:

npm install edge
npm install edge-sql
فرمان اول باعث نصب Edge.js و دومین فرمان سبب نصب پشتیبانی از SQL Server می‌شود.

Hello World

ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:
var edge = require('edge');

// The text in edge.func() is C# code
var helloWorld = edge.func('async (input) => { return input.ToString(); }');

helloWorld('Hello World!', function (error, result) {
    if (error) throw error;
    console.log(result);
});
حالا برای اجرای این Node.js application از طریق command prompt کافی است به صورت زیر عمل کنید:
node server.js
همانطور که مشاهده می‌کنید "!Hello World" در خروجی چاپ شد.

ایجاد پایگاه داده تست

در مثال‌های بعدی، نیاز به یک پایگاه داده داریم تا query‌ها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، می‌توانید نسخه‌ی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.

  1. در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
  2. بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
  3. اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO

CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO

INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
نتیجه‌ی اجرای کد بالا، ایجاد جدولی با نام SampleUsers و درج 5 رکورد در آن می‌شود.

تنظیمات ConnectionString

قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.

set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
این متغیر تنها برای command prompt جاری تعریف شده است و با بستن آن از دست می‌رود. در صورتیکه از Node.js Tools for Visual Studio استفاده می‌کنید، نیاز به ایجاد یک متغیر محیطی دائمی و راه اندازی مجدد VS دارید. همچنین در صورتیکه بخواهید متغیر محیطی دائمی ایجاد کنید، فرمان زیر را اجرا کنید:
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"


روش اول: اجرای مستقیم SQL Server Query در Edge.js

فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:

var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

var getTopUsers = edge.func('sql', function () {/*
    SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC
*/});

function logError(err, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write("Error: " + err);
    res.end("");
}    

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });

    getTopUsers(null, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
        }
    });
}).listen(port);
console.log("Node server listening on port " + port);
سپس با استفاده از command prompt، فرمان زیر را اجرا کنید:
node server-sql-query.js
حال مرورگر خود را باز و سپس آدرس http://localhost:8080 را باز کنید. در صورتی که همه چیز به درستی انجام گرفته باشد لیستی از 3 کاربر را خواهید دید.

روش دوم: اجرای کد دات نت برای SQL Server Query

Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی می‌کند. در حال حاضر از store procedures و مجموعه‌ای از کد SQL پشتیبانی نمی‌کند. بنابراین، اگر چیزی بیشتر از عملیات CRUD می‌خواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.

یادتان باشد، همیشه async

مدل اجرایی Node.js به صورت یک حلقه‌ی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js می‌شود.

ایجاد یک Class Library

اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace EdgeSampleLibrary
{
     public class Sample1
    {
        public async Task<object> Invoke(object input)
        {
            // Edge marshalls data to .NET using an IDictionary<string, object>
            var payload = (IDictionary<string, object>) input;
            var pageNumber = (int) payload["pageNumber"];
            var pageSize = (int) payload["pageSize"];
            return await QueryUsers(pageNumber, pageSize);
        }

        public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize)
        {
            // Use the same connection string env variable
            var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING");
            if (connectionString == null)
                throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable.");

            // Paging the result set using a common table expression (CTE).
            // You may rather do this in a stored procedure or use an 
            // ORM that supports async.
            var sql = @"
DECLARE @RowStart int, @RowEnd int;
SET @RowStart = (@PageNumber - 1) * @PageSize + 1;
SET @RowEnd = @PageNumber * @PageSize;

WITH Paging AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum,
            Id, FirstName, LastName, Email, CreateDate
    FROM    SampleUsers
)
SELECT  Id, FirstName, LastName, Email, CreateDate
FROM    Paging
WHERE   RowNum BETWEEN @RowStart AND @RowEnd
ORDER BY RowNum;
";
            var users = new List<SampleUser>();

            using (var cnx = new SqlConnection(connectionString))
            {
                using (var cmd = new SqlCommand(sql, cnx))
                {
                    await cnx.OpenAsync();

                    cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber });
                    cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize });

                    using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
                    {
                        while (await reader.ReadAsync())
                        {
                            var user = new SampleUser
                            {
                                Id = reader.GetInt32(0), 
                                FirstName = reader.GetString(1), 
                                LastName = reader.GetString(2), 
                                Email = reader.GetString(3), 
                                CreateDate = reader.GetDateTime(4)
                            };
                           users.Add(user);
                        }
                    }
                }
            }
            return users;
        } 
    }

    public class SampleUser
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime CreateDate { get; set; }
    }
}
سپس ذخیره و کامپایل کنید. فایل DLL خروجی که در مسیر
[project]/bin/Debug/EdgeSampleLibrary.dll
قرار دارد را در پوشه‌ی پروژه Node کپی کنید. فایل جدیدی را با نام server-dotnet-query.js در پروژه Node ایجاد کنید و کد زیر را در آن وارد کنید:
var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

// Set up the assembly to call from Node.js 
var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' });

function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); }

http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' });

    // This is the data we will pass to .NET
    var data = { pageNumber: 1, pageSize: 3 };

    // Invoke the .NET function
    querySample(data, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
            res.end("No results");
        }
    });

}).listen(port);

console.log("Node server listening on port " + port);
سپس از طریق command prompt آن را اجرا کنید:
node server-dotnet-query.js
حال مرورگر خود را باز کرده و به آدرس http://localhost:8080 بروید. در صورتیکه همه چیز به درستی انجام گرفته باشد، لیستی از 3 کاربر را خواهید دید. مقادیر pageNumber و pageSize را در فایل جاوااسکریپت تغییر دهید و تاثیر آن را بر روی خروجی مشاهده کنید.
 
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، می‌توانید از ماژول connect استفاده کنید.
نظرات مطالب
ویژگی Batching در EF Core
کدهای معادل مطلب فوق در EF 6.x را از اینجا دریافت کنید: EF6TestBatching.zip
این کدها برای حالت انجام 2 به روز رسانی و 6 ثبت، کدهای SQL زیر را تولید می‌کنند:
UPDATE [dbo].[Blogs]
SET [Url] = @0
WHERE ([BlogId] = @1)
-- @0: 'http://sample.com/blogs/dogs' (Type = String, Size = -1)
-- @1: '1' (Type = Int32)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 19 ms with result: 1

UPDATE [dbo].[Blogs]
SET [Url] = @0
WHERE ([BlogId] = @1)
-- @0: 'http://sample.com/blogs/cats' (Type = String, Size = -1)
-- @1: '2' (Type = Int32)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 47 ms with result: 1

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Horse Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/horses' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 31 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Snake Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/snakes' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 73 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Fish Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/fish' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 49 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Koala Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/koalas' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 49 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Parrot Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/parrots' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 26 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Kangaroo Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/kangaroos' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:42 ب.ظ +04:30
-- Completed in 38 ms with result: SqlDataReader
یعنی در کل 8 بار رفت و برگشت به بانک اطلاعاتی صورت گرفته‌است (هر Executing در اینجا یعنی یکبار رفت و برگشت)؛ در مقابل تنها یکبار رفت و برگشت به بانک اطلاعاتی در حالت استفاده‌ی از EF Core (با تنظیمات پیش فرض آن).
جمع کل مدت زمان عملیات در اینجا 332 میلی ثانیه است در مقایسه با 44 میلی ثانیه EF Core. یعنی EF 6.x در حالت insert/update/delete چیزی حدود 86 درصد از نمونه‌ی EF Core کندتر است و این مورد اهمیت batching و کاهش تعداد رفت و برگشت‌های به بانک اطلاعاتی است که به صورت پیش فرض در EF Core فعال است.
مطالب
EF Code First #2

در قسمت قبل با تنظیمات و قراردادهای ابتدایی EF Code first آشنا شدیم، هرچند این تنظیمات حجم کدنویسی ابتدایی راه اندازی سیستم را به شدت کاهش می‌دهند، اما کافی نیستند. در این قسمت نگاهی سطحی و مقدماتی خواهیم داشت بر امکانات مهیا جهت تنظیم ویژگی‌های مدل‌های برنامه در EF Code first.

تنظیمات EF Code first توسط اعمال متادیتای خواص

اغلب متادیتای مورد نیاز جهت اعمال تنظیمات EF Code first در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند. بنابراین اگر مدل‌های خود را در اسمبلی و پروژه class library جداگانه‌ای تعریف و نگهداری می‌کنید (مثلا به نام DomainClasses)، نیاز است ابتدا ارجاعی را به این اسمبلی به پروژه جاری اضافه نمائیم. همچنین تعدادی دیگر از متادیتای قابل استفاده در خود اسمبلی EntityFramework.dll قرار دارند. بنابراین در صورت نیاز باید ارجاعی را به این اسمبلی نیز اضافه نمود.
همان مثال قبل را در اینجا ادامه می‌دهیم. دو کلاس Blog و Post در آن تعریف شده (به این نوع کلاس‌ها POCO – the Plain Old CLR Objects نیز گفته می‌شود)، به همراه کلاس Context که از کلاس DbContext مشتق شده است. ابتدا دیتابیس قبلی را دستی drop کنید. سپس در کلاس Blog، خاصیت public int Id را مثلا به public int MyTableKey تغییر دهید و پروژه را اجرا کنید. برنامه بلافاصله با خطای زیر متوقف می‌شود:

One or more validation errors were detected during model generation:
\tSystem.Data.Entity.Edm.EdmEntityType: : EntityType 'Blog' has no key defined.

زیرا EF Code first در این کلاس خاصیتی به نام Id یا BlogId را نیافته‌است و امکان تشکیل Primary key جدول را ندارد. برای رفع این مشکل تنها کافی است ویژگی Key را به این خاصیت اعمال کنیم:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }

همچنین تعدادی ویژگی دیگر مانند MaxLength و Required را نیز می‌توان بر روی خواص کلاس اعمال کرد:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }

[MaxLength(100)]
public string Title { set; get; }

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

public IList<Post> Posts { set; get; }
}
}

این ویژگی‌ها دو مقصود مهم را برآورده می‌سازند:
الف) بر روی ساختار بانک اطلاعاتی تشکیل شده تاثیر دارند:

CREATE TABLE [dbo].[Blogs](
[MyTableKey] [int] IDENTITY(1,1) NOT NULL,
[Title] [nvarchar](100) NULL,
[AuthorName] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED
(
[MyTableKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

همانطور که ملاحظه می‌کنید در اینجا طول فیلد Title به 100 تنظیم شده است و همچنین فیلد AuthorName اینبار NOT NULL است. به علاوه primary key نیز بر اساس ویژگی Key اعمالی تعیین شده است.
البته برای اجرای کدهای تغییر کرده مدل، فعلا بانک اطلاعاتی قبلی را دستی می‌توان حذف کرد تا بتوان به ساختار جدید رسید. در مورد جزئیات مبحث DB Migration در قسمت‌های بعدی مفصلا بحث خواهد شد.

ب) اعتبار سنجی اطلاعات پیش از ارسال کوئری به بانک اطلاعاتی
برای مثال اگر در حین تعریف وهله‌ای از کلاس Blog، خاصیت AuthorName مقدار دهی نگردد، پیش از اینکه رفت و برگشتی به بانک اطلاعاتی صورت گیرد، یک validation error را دریافت خواهیم کرد. یا برای مثال اگر طول اطلاعات خاصیت Title بیش از 100 حرف باشد نیز مجددا در حین ثبت اطلاعات، یک استثنای اعتبار سنجی را مشاهده خواهیم کرد. البته امکان تعریف پیغام‌های خطای سفارشی نیز وجود دارد. برای این حالت تنها کافی است پارامتر ErrorMessage این ویژگی‌ها را مقدار دهی کرد. برای مثال:
[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]
public string AuthorName { set; get; }

نکته‌ی مهمی که در اینجا وجود دارد، وجود یک اکوسیستم هماهنگ و سازگار است. این نوع اعتبار سنجی هم با EF Code first هماهنگ است و هم برای مثال در ASP.NET MVC به صورت خودکار جهت اعتبار سنجی سمت سرور و کلاینت یک مدل می‌تواند مورد استفاده قرار گیرد و مفاهیم و روش‌های مورد استفاده در آن نیز یکی است.


تنظیمات EF Code first به کمک Fluent API

اگر علاقمند به استفاده از متادیتا، جهت تعریف قیود و ویژگی‌های خواص کلاس‌های مدل خود نیستید، روش دیگری نیز در EF Code first به نام Fluent API تدارک دیده شده است. در اینجا امکان تعریف همان ویژگی‌ها توسط کدنویسی نیز وجود دارد، به علاوه اعمال قیود دیگری که توسط متادیتای مهیا قابل تعریف نیستند.
محل تعریف این قیود، کلاس Context که از کلاس DbContext مشتق شده است، می‌باشد و در اینجا، کار با تحریف متد OnModelCreating شروع می‌شود:

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; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasKey(x => x.MyTableKey);
modelBuilder.Entity<Blog>().Property(x => x.Title).HasMaxLength(100);
modelBuilder.Entity<Blog>().Property(x => x.AuthorName).IsRequired();

base.OnModelCreating(modelBuilder);
}
}
}

به کمک پارامتر modelBuilder، امکان دسترسی به متدهای تنظیم کننده ویژگی‌های خواص یک مدل یا موجودیت وجود دارد. در اینجا چون می‌توان متدها را به صورت یک زنجیره به هم متصل کرد و همچنین حاصل نهایی شبیه به جمله بندی انگلیسی است، به آن Fluent API یا API روان نیز گفته می‌شود.
البته در این حالت امکان تعریف ErrorMessage وجود ندارد و برای این منظور باید از همان data annotations استفاده کرد.


نحوه مدیریت صحیح تعاریف نگاشت‌ها به کمک Fluent API

OnModelCreating محل مناسبی جهت تعریف حجم انبوهی از تنظیمات کلاس‌های مختلف مدل‌های برنامه نیست. در حد سه چهار سطر مشکلی ندارد اما اگر بیشتر شد بهتر است از روش زیر استفاده شود:

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

namespace EF_Sample01
{
public class BlogConfig : EntityTypeConfiguration<Blog>
{
public BlogConfig()
{
this.Property(x => x.Id).HasColumnName("MyTableKey");
this.Property(x => x.RowVersion).HasColumnType("Timestamp");
}
}


با ارث بری از کلاس EntityTypeConfiguration،‌ می‌توان به ازای هر کلاس مدل، تنظیمات را جداگانه انجام داد. به این ترتیب اصل SRP یا Single responsibility principle نقض نخواهد شد. سپس برای استفاده از این کلاس‌های Config تک مسئولیتی به نحو زیر می‌توان اقدام کرد:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new BlogConfig());




نحوه تنظیمات ابتدایی نگاشت کلاس‌ها به بانک اطلاعاتی در EF Code first

الزامی ندارد که EF Code first حتما با یک بانک اطلاعاتی از نو تهیه شده بر اساس پیش فرض‌های آن کار کند. در اینجا می‌توان از بانک‌های اطلاعاتی موجود نیز استفاده کرد. اما در این حالت نیاز خواهد بود تا مثلا نام جدولی خاص با کلاسی مفروض در برنامه، یا نام فیلدی خاص که مطابق استانداردهای نامگذاری خواص در سی شارپ تعریف نشده، با خاصیتی در یک کلاس تطابق داده شوند. برای مثال اینبار تعاریف کلاس Blog را به نحو زیر تغییر دهید:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
[Table("tblBlogs")]
public class Blog
{
[Column("MyTableKey")]
public int Id { set; get; }

[MaxLength(100)]
public string Title { set; get; }

[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]
public string AuthorName { set; get; }

public IList<Post> Posts { set; get; }

[Timestamp]
public byte[] RowVersion { set; get; }
}
}

در اینجا فرض بر این است که نام جدول متناظر با کلاس Blog در بانک اطلاعاتی مثلا tblBlogs است و نام خاصیت Id در بانک اطلاعاتی مساوی فیلدی است به نام MyTableKey. چون نام خاصیت را مجددا به Id تغییر داده‌ایم، دیگر ضرورتی به ذکر ویژگی Key وجود نداشته است. برای تعریف این دو از ویژگی‌های Table و Column جهت سفارشی سازی نام‌های خواص و کلاس استفاده شده است.
یا اگر در کلاس خود خاصیتی محاسبه شده بر اساس سایر خواص، تعریف شده است و قصد نداریم آن‌را به فیلدی در بانک اطلاعاتی نگاشت کنیم، می‌توان از ویژگی NotMapped برای مزین سازی و تعریف آن کمک گرفت.
به علاوه اگر از نام پیش فرض کلید خارجی تشکیل شده خرسند نیستید می‌توان به کمک ویژگی ForeignKey، نسبت به تعریف مقداری جدید مطابق تعاریف یک بانک اطلاعاتی موجود، اقدام کرد.
همچنین خاصیت دیگری به نام RowVersion در اینجا اضافه شده که با ویژگی TimeStamp مزین گردیده است. از این خاصیت ویژه برای بررسی مسایل همزمانی ثبت اطلاعات در EF استفاده می‌شود. به علاوه بانک اطلاعاتی می‌تواند به صورت خودکار آن‌را در حین ثبت مقدار دهی کند.
تمام این تغییرات را به کمک Fluent API نیز می‌توان انجام داد:

modelBuilder.Entity<Blog>().ToTable("tblBlogs");
modelBuilder.Entity<Blog>().Property(x => x.Id).HasColumnName("MyTableKey");
modelBuilder.Entity<Blog>().Property(x => x.RowVersion).HasColumnType("Timestamp");



تبدیل پروژه‌های قدیمی EF به کلاس‌های EF Code first به صورت خودکار

روش متداول کار با EF از روز اول آن، مهندسی معکوس خودکار اطلاعات یک بانک اطلاعاتی و تبدیل آن به یک فایل EDMX بوده است. هنوز هم می‌توان از این روش در اینجا نیز بهره جست. برای مثال اگر قصد دارید یک پروژه قدیمی را تبدیل به نمونه جدید Code first کنید، یا یک بانک اطلاعاتی موجود را مهندسی معکوس کنید، بر روی پروژه در Solution explorer کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس از صفحه ظاهر شده، ADO.NET Entity data model را انتخاب کرده و در ادامه گزینه «Generate from database» را انتخاب کنید. این روال مرسوم کار با EF Database first است.
پس از اتمام کار به entity data model designer مراجعه کرده و بر روی صفحه کلیک راست نمائید. از منوی ظاهر شده گزینه «Add code generation item» را انتخاب کنید. سپس در صفحه باز شده از لیست قالب‌های موجود، گزینه «ADO.NET DbContext Generator» را انتخاب نمائید. این گزینه به صورت خودکار اطلاعات فایل EDMX قدیمی یا موجود شما را تبدیل به کلاس‌های مدل Code first معادل به همراه کلاس DbContext معرف آن‌ها خواهد کرد.

روش دیگری نیز برای انجام اینکار وجود دارد. نیاز است افزونه‌ی به نام Entity Framework Power Tools را دریافت کنید. پس از نصب، از منوی Entity Framework آن گزینه‌ی «Reverse Engineer Code First» را انتخاب نمائید. در اینجا می‌توان مشخصات اتصال به بانک اطلاعاتی را تعریف و سپس نسبت به تولید خودکار کدهای مدل‌ها و DbContext مرتبط اقدام کرد.



استراتژی‌های مقدماتی تشکیل بانک اطلاعاتی در EF Code first

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

System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
// or
System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseAlways<Context>());

می‌توان بانک اطلاعاتی را در صورت تغییر اطلاعات یک مدل به صورت خودکار drop کرده و نسبت به ایجاد نمونه‌ای جدید اقدام کرد (DropCreateDatabaseIfModelChanges)؛ یا در حین آزمایش برنامه همیشه (DropCreateDatabaseAlways) با شروع برنامه، ابتدا باید بانک اطلاعاتی drop شده و سپس نمونه جدیدی تولید گردد.
محل فراخوانی این دستور هم باید در نقطه آغازین برنامه، پیش از وهله سازی اولین DbContext باشد. مثلا در برنامه‌های وب در متد Application_Start فایل global.asax.cs یا در برنامه‌های WPF در متد سازنده کلاس App می‌توان بانک اطلاعاتی را آغاز نمود.
البته الزامی به استفاده از کلاس‌های DropCreateDatabaseIfModelChanges یا DropCreateDatabaseAlways وجود ندارد. می‌توان با پیاده سازی اینترفیس IDatabaseInitializer از نوع کلاس Context تعریف شده در برنامه، همان عملیات را شبیه سازی کرد یا سفارشی نمود:

public class MyInitializer : IDatabaseInitializer<Context>
{
public void InitializeDatabase(Context context)
{
if (context.Database.Exists() ||
context.Database.CompatibleWithModel(throwIfNoMetadata: false))
context.Database.Delete();

context.Database.Create();
}
}

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

System.Data.Entity.Database.SetInitializer(new MyInitializer());


نکته:
اگر از یک بانک اطلاعاتی موجود استفاده می‌کنید (محیط کاری) و نیازی به پیش فرض‌های EF Code first ندارید و همچنین این بانک اطلاعاتی نیز نباید drop شود یا تغییر کند، می‌توانید تمام این پیش فرض‌ها را با دستور زیر غیرفعال کنید:

Database.SetInitializer<Context>(null);

بدیهی است این دستور نیز باید پیش از ایجاد اولین وهله از شیء DbContext فراخوانی شود.


همچنین باید درنظر داشت که در آخرین نگارش‌های پایدار EF Code first، این موارد بهبود یافته‌اند و مبحثی تحت عنوان DB Migration ایجاد شده است تا نیازی نباشد هربار بانک اطلاعاتی drop شود و تمام اطلاعات از دست برود. می‌توان صرفا تغییرات کلاس‌ها را به بانک اطلاعاتی اعمال کرد که به صورت جداگانه، در قسمتی مجزا بررسی خواهد شد. به این ترتیب دیگر نیازی به drop بانک اطلاعاتی نخواهد بود. به صورت پیش فرض در صورت از دست رفتن اطلاعات یک استثناء را سبب خواهد شد (که توسط برنامه نویس قابل تنظیم است) و در حالت خودکار یا دستی با تنظیمات ویژه قابل اعمال است.



تنظیم استراتژی‌های آغاز بانک اطلاعاتی در فایل کانفیگ برنامه

الزامی ندارد که حتما متد Database.SetInitializer را دستی فراخوانی کنیم. با اندکی تنظیم فایل‌های app.config و یا web.config نیز می‌توان نوع استراتژی مورد استفاده را تعیین کرد:

<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="MyNamespace.MyInitializerClass, MyAssembly" />
</appSettings>

<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="Disabled" />
</appSettings>

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

<appSettings>
<add key="DatabaseInitializerForType EF_Sample01.Context, EF_Sample01"
value="EF_Sample01.MyInitializer, EF_Sample01" />
</appSettings>



اجرای کدهای ویژه در حین تشکیل یک بانک اطلاعاتی جدید

امکان سفارشی سازی این آغاز کننده‌های پیش فرض نیز وجود دارد. برای مثال:

public class MyCustomInitializer : DropCreateDatabaseIfModelChanges<Context>
{
protected override void Seed(Context context)
{
context.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
context.Database.ExecuteSqlCommand("CREATE INDEX IX_title ON tblBlogs (title)");
base.Seed(context);
}
}

در اینجا با ارث بری از کلاس DropCreateDatabaseIfModelChanges یک آغاز کننده سفارشی را تعریف کرده‌ایم. سپس با تحریف متد Seed آن می‌توان در حین آغاز یک بانک اطلاعاتی، تعدادی رکورد پیش فرض را به آن افزود. کار ذخیره سازی نهایی در متد base.Seed انجام می‌شود.
برای استفاده از آن اینبار در حین فراخوانی متد System.Data.Entity.Database.SetInitializer، از کلاس MyCustomInitializer استفاده خواهیم کرد.
و یا توسط متد context.Database.ExecuteSqlCommand می‌توان دستورات SQL را مستقیما در اینجا اجرا کرد. عموما دستوراتی در اینجا مدنظر هستند که توسط ORMها پشتیبانی نمی‌شوند. برای مثال تغییر collation یک ستون یا افزودن یک ایندکس و مواردی از این دست.


سطح دسترسی مورد نیاز جهت فراخوانی متد Database.SetInitializer

استفاده از متدهای آغاز کننده بانک اطلاعاتی نیاز به سطح دسترسی بر روی بانک اطلاعاتی master را در SQL Server دارند (زیرا با انجام کوئری بر روی این بانک اطلاعاتی مشخص می‌شود، آیا بانک اطلاعاتی مورد نظر پیشتر تعریف شده است یا خیر). البته این مورد حین کار با SQL Server CE شاید اهمیتی نداشته باشد. بنابراین اگر کاربری که با آن به بانک اطلاعاتی متصل می‌شویم سطح دسترسی پایینی دارد نیاز است Persist Security Info=True را به رشته اتصالی اضافه کرد. البته این مورد را پس از انجام تغییرات بر روی بانک اطلاعاتی جهت امنیت بیشتر حذف کنید (یا به عبارتی در محیط کاری Persist Security Info=False باید باشد).

Server=(local);Database=yourDatabase;User ID=yourDBUser;Password=yourDBPassword;Trusted_Connection=False;Persist Security Info=True


تعیین Schema و کاربر فراخوان دستورات SQL

در EF Code first به صورت پیش فرض همه چیز بر مبنای کاربری با دسترسی مدیریتی یا dbo schema در اس کیوال سرور تنظیم شده است. اما اگر کاربر خاصی برای کار با دیتابیس تعریف گردد که در هاست‌های اشتراکی بسیار مرسوم است، دیگر از دسترسی مدیریتی dbo خبری نخواهد بود. اینبار نام جداول ما بجای dbo.tableName مثلا someUser.tableName می‌باشند و عدم دقت به این نکته، اجرای برنامه را غیرممکن می‌سازد.
برای تغییر و تعیین صریح کاربر متصل شده به بانک اطلاعاتی اگر از متادیتا استفاده می‌کنید، روش زیر باید بکارگرفته شود:

[Table("tblBlogs", Schema="someUser")]    
public class Blog

و یا در حالت بکارگیری Fluent API به نحو زیر قابل تنظیم است:

modelBuilder.Entity<Blog>().ToTable("tblBlogs", schemaName:"someUser");






مطالب
کش خروجی API در ASP.NET Core با Redis
در این مقاله نمی‌خواهیم به طور عمیقی وارد جزییاتی مثل توضیح Redis یا کش بشویم؛ فرض شده‌است که کاربر با این مفاهیم آشناست. به طور خلاصه کش کردن یعنی همیشه به دیتابیس یا هارددیسک برای گرفتن اطلاعاتی که می‌خواهیم و گرفتنش هم کند است، وصل نشویم و بجای آن، اطلاعات را در یک محل موقتی که گرفتنش خیلی سریعتر بوده قرار دهیم و برای استفاده به آنجا برویم و اطلاعات را با سرعت بالا بخوانیم. کش کردن هم دسته بندی‌های مختلفی دارد که بر حسب سناریوهای مختلفی که وجود دارد، کاربرد خود را دارند. مثلا ساده‌ترین کش در ASP.NET Core، کش محلی (In-Memory Cache) می‌باشد که اینترفیس IMemoryCache را اعمال می‌کند و نیازی به هیچ پکیجی ندارد و به صورت درونی در ASP.NET Core در دسترس است که برای حالت توسعه، یا حالتیکه فقط یک سرور داشته باشیم، مناسب است؛ ولی برای برنامه‌های چند سروری، نوع دیگری از کش که به اصطلاح به آن Distributed Cache می‌گویند، بهتر است استفاده شود. چند روش برای پیاده‌سازی با این ساختار وجود دارد که نکته مشترکشان اعمال اینترفیس واحد IDistributedCache می‌باشد. در نتیجه‌ی آن، تغییر ساختار کش به روش‌های دیگر، که اینترفیس مشابهی را اعمال می‌کنند، با کمترین زحمت صورت می‌گیرد. این روش‌ها به طور خیلی خلاصه شامل موارد زیر می‌باشند: 

1- Distributed Memory Cache: در واقع Distributed نیست و کش معمولی است؛ فقط برای اعمال اینترفیس IDistributedCache که امکان تغییر آن در ادامه‌ی توسعه نرم‌افزار میسر باشد، این روش توسط مایکروسافت اضافه شده‌است. نیاز به نصب پکیجی را ندارد و به صورت توکار در ASP.NET Core در دسترس است.
2- Distributed SQL Server Cache: کاربرد چندانی ندارد. با توجه به اینکه هدف اصلی از کش کردن، افزایش سرعت و عدم اتصال به دیتابیس است، استفاده از حافظه‌ی رم، بجای دیتابیس ترجیح داده می‌شود.
3- Distributed Redis Cache: استفاده از Redis که به طور خلاصه یک دیتابیس Key/Value در حافظه است. سرعت بالایی دارد و محبوب‌ترین روش بین برنامه‌نویسان است. برای اعمال آن در ASP.NET Core نیاز به نصب پکیج می‌باشد.

موارد بالا انواع زیرساخت و ساختار (Cache Provider) برای پیاده‌سازی کش می‌باشند. روش‌های مختلفی برای استفاده از این Cache Providerها وجود دارد. مثلا یک روش، استفاده مستقیم در کدهای درونی متد یا کلاسمان می‌باشد و یا در روش دیگر می‌توانیم به صورت یک Middleware این پروسه را مدیریت کنیم، یا در روش دیگر (که موضوع این مقاله است) از ActionFilterAttribute استفاده می‌کنیم. یکی از روش‌های جالب دیگر کش کردن، اگر از Entity Framework به عنوان ORM استفاده می‌کنیم، استفاده از سطح دوم کش آن (EF Second Level Cache) می‌باشد. EF دو سطح کش دارد که سطح اول آن توسط خود Context به صورت درونی استفاده می‌شود و ما می‌توانیم از سطح دوم آن استفاده کنیم. مزیت آن به نسبت روش‌های قبلی این است که نتیجه‌ی کوئری ما (که با عبارات لامبدا نوشته می‌شود) را کش می‌کند و علاوه بر امکان تنظیم زمان انقضا برای این کش، در صورت تغییر یک entity خاص (انجام عملیات Update/Insert/Delete) خود به خود، کش کوئری مربوط به آن entity پاک می‌شود تا با مقدار جدید آن جایگزین شود که روش‌های دیگر این مزیت را ندارند. در این مقاله قرار نیست در مورد این روش کش صحبت کنیم. استفاده از این روش کش به صورت توکار در EF Core وجود ندارد و برای استفاده از آن در صورتی که از EF Core قبل از ورژن 3 استفاده می‌کنید می‌توانید از پکیج  EFSecondLevelCache.Core  و در صورت استفاده از EF Core 3 از پکیج  EF Core Second Level Cache Interceptor  استفاده نمایید که در هر دو حالت می‌توان هم از Memory Cache Provider و هم از Redis Cache Provider استفاده نمود.

در این مقاله می‌خواهیم Responseهای APIهایمان را در یک پروژه‌ی Web API، به ساده‌ترین حالت ممکن کش کنیم. زیرساخت این کش می‌تواند هر کدام از موارد ذکر شده‌ی بالا باشد. در این مقاله از Redis برای پیاده‌سازی آن استفاده می‌کنیم که با نصب پکیج Microsoft.Extensions.Caching.StackExchangeRedis انجام می‌گیرد. این بسته‌ی نیوگت که متعلق به مایکروسافت بوده و روش پایه‌ی استفاده از Redis در ASP.NET Core است، اینترفیس IDistributedCache را اعمال می‌کند:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

سپس اینترفیس IResponseCacheService را می‌سازیم تا از این اینترفیس به جای IDistributedCache استفاده کنیم. البته می‌توان از IDistributedCache به طور مستقیم استفاده کرد؛ ولی چون همه‌ی ویژگی‌های این اینترفیس را نمی‌خواهیم و هم اینکه می‌خواهیم serialize کردن نتایج API را در کلاسی که از این اینترفیس ارث‌بری می‌کند (ResponseCacheService) بیاوریم (تا آن را کپسوله‌سازی (Encapsulation) کرده باشیم تا بعدا بتوانیم مثلا بجای پکیج Newtonsoft.Json، از System.Text.Json برای serialize کردن‌ها استفاده کنیم):
public interface IResponseCacheService
    {
        Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive);
        Task<string> GetCachedResponseAsync(string cacheKey);
    }
یادآوری: Redis قابلیت ذخیره‌ی داده‌هایی از نوع آرایه‌ی بایت‌ها را دارد (و نه هر نوع دلخواهی را). بنابراین اینجا ما بجای ذخیره‌ی مستقیم نتایج APIهایمان (که ممکن نیست)، می‌خواهیم ابتدا آن‌ها را با serialize کردن به نوع رشته‌ای (که فرمت json دارد) تبدیل کنیم و سپس آن را ذخیره نماییم.

حالا کلاس ResponseCacheService که این اینترفیس را اعمال می‌کند می‌سازیم: 
    public class ResponseCacheService : IResponseCacheService, ISingletonDependency
    {
        private readonly IDistributedCache _distributedCache;

        public ResponseCacheService(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }

        public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive)
        {
            if (response == null) return;
            var serializedResponse = JsonConvert.SerializeObject(response);
            await _distributedCache.SetStringAsync(cacheKey, serializedResponse, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = timeToLive
            });
        }

        public async Task<string> GetCachedResponseAsync(string cacheKey)
        {
            var cachedResponse = await _distributedCache.GetStringAsync(cacheKey);
            return string.IsNullOrWhiteSpace(cachedResponse) ? null : cachedResponse;
        }
    }
دقت کنید که اینترفیس IDistributedCache در این کلاس استفاده شده است. اینترفیس ISingletonDependency صرفا یک اینترفیس نشان گذاری برای اعمال خودکار ثبت سرویس به صورت Singleton می‌باشد (اینترفیس را خودمان ساخته‌ایم و آن را برای رجیستر راحت سرویس‌هایمان تنظیم کرده‌ایم). اگر نمی‌خواهید از این روش برای ثبت این سرویس استفاده کنید، می‌توانید به صورت عادی این سرویس را رجیستر کنید که در ادامه، در قسمت مربوطه به صورت کامنت شده آمده است.

حالا کدهای لازم برای رجیستر کردن Redis و تنظیمات آن را در برنامه اضافه می‌کنیم. قدم اول ایجاد یک کلاس POCO به نام RedisCacheSettings است که به فیلدی به همین نام در appsettings.json نگاشت می‌شود:
public class RedisCacheSettings
    {
        public bool Enabled { get; set; }
        public string ConnectionString { get; set; }
        public int DefaultSecondsToCache { get; set; }
    }

این فیلد را در appsettings.json هم اضافه می‌کنیم تا در استارتاپ برنامه، با مپ شدن به کلاس RedisCacheSettings، قابلیت استفاده شدن در تنظیمات Redis را داشته باشد. 
"RedisCacheSettings": {
      "Enabled": true,
      "ConnectionString": "192.168.1.107:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0,connectTimeout=500,connectRetry=3",
      "DefaultSecondsToCache": 600
    },

  حالا باید سرویس Redis را در متد ConfigureServices، به همراه تنظیمات آن رجیستر کنیم. می‌توانیم کدهای مربوطه را مستقیم در متد ConfigureServices بنویسیم و یا به صورت یک متد الحاقی در کلاس جداگانه بنویسیم و از آن در ConfigureServices استفاده کنیم و یا اینکه از روش Installer برای ثبت خودکار سرویس و تنظیماتش استفاده کنیم. اینجا از روش آخر استفاده می‌کنیم. برای این منظور کلاس CacheInstaller را می‌سازیم: 
    public class CacheInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            var redisCacheService = appSettings.RedisCacheSettings;
            services.AddSingleton(redisCacheService);

            if (!appSettings.RedisCacheSettings.Enabled) return;

            services.AddStackExchangeRedisCache(options =>
                options.Configuration = appSettings.RedisCacheSettings.ConnectionString);

            // Below code applied with ISingletonDependency Interface
            // services.AddSingleton<IResponseCacheService, ResponseCacheService>();
        }
    }

خب تا اینجا اینترفیس اختصاصی خودمان را ساختیم و Redis را به همراه تنظیمات آن، رجیستر کردیم. برای اعمال کش، چند روش وجود دارد که همانطور که گفته شد، اینجا از روش ActionFilterAttribute استفاده می‌کنیم که یکی از راحت‌ترین راه‌های اعمال کش در APIهای ماست. کلاس CachedAttribute را ایجاد می‌کنیم:
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class CachedAttribute : Attribute, IAsyncActionFilter
    {
        private readonly int _secondsToCache;
        private readonly bool _useDefaultCacheSeconds;
        public CachedAttribute()
        {
            _useDefaultCacheSeconds = true;
        }
        public CachedAttribute(int secondsToCache)
        {
            _secondsToCache = secondsToCache;
            _useDefaultCacheSeconds = false;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<RedisCacheSettings>();

            if (!cacheSettings.Enabled)
            {
                await next();
                return;
            }

            var cacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>();

            // Check if request has Cache
            var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request);
            var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey);

            // If Yes => return Value
            if (!string.IsNullOrWhiteSpace(cachedResponse))
            {
                var contentResult = new ContentResult
                {
                    Content = cachedResponse,
                    ContentType = "application/json",
                    StatusCode = 200
                };
                context.Result = contentResult;
                return;
            }

            // If No => Go to method => Cache Value
            var actionExecutedContext = await next();

            if (actionExecutedContext.Result is OkObjectResult okObjectResult)
            {
                var secondsToCache = _useDefaultCacheSeconds ? cacheSettings.DefaultSecondsToCache : _secondsToCache;
                await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value,
                    TimeSpan.FromSeconds(secondsToCache));
            }
        }

        private static string GenerateCacheKeyFromRequest(HttpRequest httpRequest)
        {
            var keyBuilder = new StringBuilder();
            keyBuilder.Append($"{httpRequest.Path}");
            foreach (var (key, value) in httpRequest.Query.OrderBy(x => x.Key))
            {
                keyBuilder.Append($"|{key}-{value}");
            }

            return keyBuilder.ToString();
        }
    }
در این کلاس، تزریق وابستگی‌های IResponseCacheService و RedisCacheSettings به روش خاصی انجام شده است و نمی‌توانستیم از روش Constructor Dependency Injection استفاده کنیم چون در این حالت می‌بایستی این ورودی در Controller مورد استفاده هم تزریق شود و سپس در اتریبیوت [Cached] بیاید که مجاز به اینکار نیستیم؛ بنابراین از این روش خاص استفاده کردیم. مورد دیگر فرمول ساخت کلید کش می‌باشد تا بتواند کش بودن یک Endpoint خاص را به طور خودکار تشخیص دهد که این متد در همین کلاس آمده است. 
 
حالا ما می‌توانیم با استفاده از attributeی به نام  [Cached]  که روی APIهای از نوع HttpGet قرار می‌گیرد آن‌ها را براحتی کش کنیم. کلاس بالا هم طوری طراحی شده (با دو سازنده متفاوت) که در حالت استفاده به صورت [Cached] از مقدار زمان پیشفرضی استفاده می‌کند که در فایل appsettings.json تنظیم شده است و یا اگر زمان خاصی را مد نظر داشتیم (مثال 1000 ثانیه) می‌توانیم آن را به صورت  [(Cached(1000]  بیاوریم. کلاس زیر نمونه‌ی استفاده‌ی از آن می‌باشد:
[Cached]
[HttpGet]
public IActionResult Get()
  {
    var rng = new Random();
    var weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
      Date = DateTime.Now.AddDays(index),
      TemperatureC = rng.Next(-20, 55),
      Summary = Summaries[rng.Next(Summaries.Length)]
    })
      .ToArray();
    return Ok(weatherForecasts);
  }
بنابراین وقتی تنظیمات اولیه، برای پیاده‌سازی این کش انجام شود، اعمال کردن آن به سادگی قرار دادن یک اتریبیوت ساده‌ی [Cached] روی هر apiی است که بخواهیم خروجی آن را کش کنیم. فقط توجه نمایید که این روش فقط برای اکشن‌هایی که کد 200 را بر می‌گردانند، یعنی متد Ok را return می‌کنند (OkObjectResult) کار می‌کند. بعلاوه اگر از اتریبیوت ApiResultFilter یا مفهوم مشابه آن برای تغییر خروجی API به فرمت خاص استفاده می‌کنید، باید در آن تغییرات کوچکی را انجام دهید تا با این حالت هماهنگ شود. 
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت هفتم - بررسی عملگر Nested loop‌ در یک Query Plan
دراین قسمت قصد داریم عملگر nested loop حاصل از نوشتن جوین‌ها را دقیق‌تر بررسی کنیم. یک حلقه‌ی تو در تو، از هر ردیف ورودی (دیتاست خارجی) برای یافتن ردیف‌هایی (دیتاست درونی) که نوع جوین را برآورده می‌کنند، استفاده می‌کند.


بررسی مفهوم دیتاست خارجی و درونی

 ابتدا در management studio از منوی Query، گزینه‌ی Include actual execution plan را انتخاب می‌کنیم. سپس کوئری‌های زیر را اجرا می‌کنیم:
USE [WideWorldImporters];
GO

SET STATISTICS IO ON;
GO


/*
What's are the inner and outer
data sets?
*/
SELECT
    [ol].[OrderLineID],
    [o].[CustomerID]
FROM [Sales].[OrderLines] [ol]
    INNER JOIN [Sales].[Orders] [o]
    ON [ol].[OrderID] = [o].[OrderID]
WHERE [o].[CustomerID] = 185;
GO
این کوئری یک جوین بین جداول OrderLines و Orders را تشکیل داده‌است؛ به همراه کوئری پلن زیر:


در اینجا دیتاست خارجی، همان index seek بالایی است که بر روی جدول Orders انجام شده‌است. اولین ردیف بازگشت داده شده‌ی توسط آن به همراه OrderID مربوطه را به حلقه‌ی تو در توی Inner Join ارسال می‌کند. سپس index seek دوم بر روی جدول OrderLines‌، بر اساس OrderID دیتاست خارجی، ردیف مرتبطی را در صورت وجود یافته و به حلقه‌ی تو در توی Inner Join بازگشت می‌دهد که در نهایت به select ارسال می‌شود و این عملیات به همین ترتیب ادامه پیدا می‌کند. این خلاصه‌ی کاری است که یک حلقه‌ی تو در تو انجام می‌دهد.

سؤال: اگر جای این دیتاست‌ها را عوض کنیم چه اتفاقی رخ خواهد داد؟
در کوئری زیر توسط گزینه‌ی FORCE ORDER سبب شده‌ایم تا جای دیتاست‌های OUTER/INNER تغییر کند (البته این query hint، کاربرد عملی ندارد و صرفا جهت نمایش دیتاست‌ها از آن استفاده کرده‌ایم):
SELECT
    [ol].[OrderLineID],
    [o].[CustomerID]
FROM [Sales].[OrderLines] [ol]
    INNER JOIN [Sales].[Orders] [o]
    ON [ol].[OrderID] = [o].[OrderID]
WHERE [o].[CustomerID] = 185
OPTION (FORCE ORDER);
اینبار در کوئری پلن تولید شده، index seek بالایی بر روی جدول OrderLines، دیتاست خارجی را تشکیل می‌دهد و index seek دوم بر روی جدول Orders، دیتاست درونی را:



یک نکته: در این تصاویر بجای nested loop، از عملگر Hash Match استفاده شده‌است. اگر بخواهیم بهینه سازی کوئری را وادار کنیم تا از nested loop استفاده کند، می‌توان کوئری فوق را توسط یک INNER LOOP JOIN به صورت زیر نوشت:
SELECT
    [ol].[OrderLineID],
    [o].[CustomerID]
FROM [Sales].[OrderLines] [ol]
    INNER LOOP JOIN [Sales].[Orders] [o]
    ON [ol].[OrderID] = [o].[OrderID]
WHERE [o].[CustomerID] = 185
OPTION (FORCE ORDER);
GO
که یک چنین کوئری پلنی را تولید می‌کند:


همانطور که مشاهده می‌کنید اینبار به علت بالا رفتن تعداد ردیف‌هایی که باید پردازش کند، به یک پلن بسیار غیر بهینه رسیده‌است که برای بهبود آن مجبور شده‌است Parallelism را نیز فعال کند.

در این حالت اگر هر سه کوئری فوق را با هم اجرا کنیم، تا بتوانیم هزینه‌ی آن‌ها را در کوئری پلن نهایی تولید شده، با یکدیگر مقایسه کنیم، هزینه‌ی کوئری اول صفر درصد، کوئری دوم 1 درصد و کوئری سوم 99 درصد نسبت به کل batch محاسبه می‌شود. علت آن را نیز در برگه‌ی messages، با مشاهده‌ی logical reads 477304 مربوط به کوئری سوم می‌توان مشاهده کرد که نسبت به سایر کوئری‌ها بسیار بیشتر است. بنابراین بهتر است در کار بهینه ساز کوئری‌ها به صورت دستی دخالت نکنیم!


بهبود کارآیی یک کوئری، با حذف حلقه‌ی تو در توی کوئری پلن آن در حالت Key lookup

کوئری زیر را با فرض انتخاب گزینه‌ی Include actual execution plan در منوی کوئری، اجرا می‌کنیم:
SELECT
    [ContactPersonID],
    [OrderDate],
    [CustomerPurchaseOrderNumber]
FROM [Sales].[Orders]
WHERE [ContactPersonID] = 3144;
این کوئری هرچند به همراه یک جوین نیست، اما دارای کوئری پلنی دارای یک nested loop است:


ایندکس‌هایی که در این کوئری پلن استفاده شده‌اند، شامل موارد پیش‌فرض زیر هستند؛ یکی بر روی OrderID که کلید اصلی جدول است، تشکیل شده و دیگری بر روی ContactPersonID که در قسمت where کوئری فوق مورد استفاده قرار گرفته‌است:
ALTER TABLE [Sales].[Orders] ADD  CONSTRAINT [PK_Sales_Orders] PRIMARY KEY CLUSTERED 
(
[OrderID] ASC
)

GO

CREATE NONCLUSTERED INDEX [FK_Sales_Orders_ContactPersonID] ON [Sales].[Orders]
(
[ContactPersonID] ASC
)
علت وجود عملگر key lookup بر روی ایندکس PK_Sales_Orders در اینجا این است که ایندکس FK_Sales_Orders_ContactPersonID، ستون‌های کوئری نوشته شده را include نکرده‌است. به همین جهت مجبور شده‌است آن‌ها را از clustered index تعریف شده دریافت کند.
برای بهبود این وضعیت، NONCLUSTERED INDEX تعریف شده را به صورت زیر تغییر می‌دهیم تا ستون‌های OrderDate و CustomerPurchaseOrderNumber را INCLUDE کند:
CREATE NONCLUSTERED INDEX [FK_Sales_Orders_ContactPersonID]
ON [Sales].[Orders] (
[ContactPersonID] ASC
)
INCLUDE (
[OrderDate], [CustomerPurchaseOrderNumber]
)
WITH (DROP_EXISTING = ON)
ON [USERDATA];
GO
اکنون اگر مجددا کوئری قبلی را اجرا کنیم:
SELECT
    [ContactPersonID],
    [OrderDate],
    [CustomerPurchaseOrderNumber]
FROM [Sales].[Orders]
WHERE [ContactPersonID] = 3144;
به این کوئری پلن دارای index seek بدون nested loop می‌رسیم:


چون ایندکس جدید تعریف شده کاملا کوئری ما را پوشش می‌دهد، دیگر نیازی به ایجاد یک nested loop، جهت کار با چندین index متفرقه نیست.


بهبود کارآیی یک کوئری، با حذف حلقه‌ی تو در توی کوئری پلن آن در حالت RID lookup

در اینجا یک جدول کپی را از روی جدول اصلی Orders ایجاد کرده‌ایم؛ به همراه تعریف یک NONCLUSTERED INDEX بر روی ستون ContactPersonID آن:
USE [WideWorldImporters]
GO

DROP TABLE [Sales].[Copy_Orders]
GO

SELECT *
INTO [Sales].[Copy_Orders]
FROM [Sales].[Orders];
GO

CREATE NONCLUSTERED INDEX [NCI_Copy_Orders_ContactPersonID]
ON [Sales].[Copy_Orders] (
[ContactPersonID]
);
GO
سپس کوئری زیر را که همانند کوئری مثال قبلی است، بر روی این جدول کپی اجرا می‌کنیم:
SELECT
    [ContactPersonID],
    [OrderDate],
    [CustomerPurchaseOrderNumber]
FROM [Sales].[Copy_Orders]
WHERE [ContactPersonID] = 3144;
نتیجه‌ی آن تولید کوئری پلن زیر است:


در اینجا یک nested loop را به همراه RID lookup داریم (RID به معنای row id است). همچنین واژه‌ی heap نیز ذکر شده‌است. در این حالت اطلاعات یک چنین جدولی بدون هیچگونه ترتیبی ذخیره شده‌اند؛ بنابراین نیاز به شماره ردیف آن (RID) برای برقراری ارتباطات می‌باشد. Key lookup زمانی رخ می‌دهند که یک جدول دارای یک clustered index باشد و RID lookup، در حالت عکس آن رخ می‌دهد. دقیقا مانند جدول کپی ایجاد شده، که دارای یک clustered index نیست.

در صورت مشاهده‌ی RID lookup نیز می‌توانیم ستون‌هایی از کوئری را که در NONCLUSTERED INDEX ذکر نشده‌اند، include کنیم:
CREATE NONCLUSTERED INDEX [NCI_Copy_Orders_ContactPersonID]
ON [Sales].[Copy_Orders] (
[ContactPersonID] ASC
)
INCLUDE (
[OrderDate], [CustomerPurchaseOrderNumber]
)
WITH (DROP_EXISTING = ON)
ON [USERDATA];
GO
و در این حالت اگر همان کوئری قبلی را مجددا اجرا کنیم، به کوئری پلن دارای index seek زیر خواهیم رسید:

مطالب
خواندن اطلاعات از فایل اکسل با استفاده از LinqToExcel
در این مقاله مروری سریع و کاربردی خواهیم داشت بر توانایی‌های مقدماتی LinqToExcel
در ابتدا می‌بایست LinqToExcel را از طریق NuGet به پروژه افزود.
PM> Install-Package LinqToExcel
و یا از طریق solution Explorer گزینه Manage NuGet Packages 

اکنون فایل اکسل ذیل را در نظر بگیرید.

روش خواندن اطلاعات از فایل اکسل فوق تحت فرامین Linq و با مشخص کردن نام sheet مورد نظر  توسط شئ ExcelQueryFactory  بصورت زیر است.

 string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
var excel = new ExcelQueryFactory(pathToExcelFile);
            string sheetName = "Sheet1";
            var persons = from a in excel.Worksheet(sheetName) select a;
            foreach (var a in persons)
            {
                MessageBox.Show(a["Name"]+" "+a["Family"]);
            }


در صورتیکه بخواهیم انتقال اطلاعات فایل اکسل به جداول بانک اطلاعاتی مانند Sql Server بطور مثال با روش EF Entity Framework را انجام دهیم کلاس زیر با نام person را فرض نمایید.

 public class Person
        {
            public string Name { get; set; }
            public string Family { get; set; }
        }
باید بدانید که بصورت پیشفرض سطر اول از فایل اکسل به عنوان نام ستون انتخاب می‌شود و می‌بایست جهت نگاشت با نام property‌های کلاس ما دقیقاً همنام باشد.

 string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
            var excel = new ExcelQueryFactory(pathToExcelFile);
            string sheetName = "Sheet1";
            var persons = from a in excel.Worksheet<Person>(sheetName) select a;
            foreach (var a in persons)
            {
                MessageBox.Show(a.Name+" "+a.Family);
            }
  اگر فایل اکسل ما ستون‌های بیشتری داشته باشد تنها ستونهای همنام با propertyهای کلاس ما به کلاس نگاشت پیدا می‌کند و سایر ستونها نادیده گرفته می‌شود.
در صورتیکه نام ستونهای فایل اکسل(سطر اول) با نام property‌های کلاس یکسان نباشد جهت نگاشت آنها در کلاس می‌توان از متد AddMapping استفاده نمود.
 

 string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
            var excel = new ExcelQueryFactory(pathToExcelFile);
            string sheetName = "Sheet1";
            excel.AddMapping("Name","نام");
            excel.AddMapping("Family", "نام خانوادگی");
            var persons = from a in excel.Worksheet<Person>(sheetName) select a;
            foreach (var a in persons)
            {
                MessageBox.Show(a.Name+" "+a.Family);
            }

در کدهای بالا در صورتی که sheetName قید نشود بصورت پیشفرض Sheet1 از فایل اکسل  انتخاب می‌شود.

var persons = from a in excel.Worksheet<Person>() select a;
همچنین می‌توان از اندیس جهت مشخص نمودن Sheet مورد نظر استفاده نمود که اندیس‌ها از صفر شروع می‌شوند.

var persons = from a in excel.Worksheet<Person>(0) select a;
توسط متد GetWorksheetNames می توان نام sheet‌ها را بدست آورد.

public IEnumerable<string> getWorkSheets()
{
string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
    
    var excel = new ExcelQueryFactory(pathToExcelFile);

    return excel.GetWorksheetNames();
}
و توسط متد GetColumnNames   می توان نام ستونها را بدست آورد.  

var SheetColumnNames = excel.GetColumnNames(sheetName);
همانطور که می‌بینید با روش توضیح داده شده در این مقاله به راحتی از فرامین Linq مانند where می‌توان در انتخاب اطلاعات از فایل اکسل استفاده نمود و سپس نتیجه را به جداول مورد نظر انتقال داد.