مطالب
ستون محاسباتی (computed column)
برخی از داده‌ها از ترکیب و ادغام شدن چند داده دیگر بدست می‌آیند. مثلا شماره دانشجویی از ترکیب چند صفت مختلف بوجود می‌آید (مثل نیمسال ورودی، کددانشگاه، کدرشته تحصیلی...).
برای پیاده سازی اینگونه ستون‌ها SQL Server یک قابلیتی به نام computed column ارائه داده است. برای تعریف این چنین ستون هایی بعد از نام ستون از کلمه AS استفاده می‌کنیم. عبارتی که ستون محاسباتی را تشکیل می‌دهد می‌تواند شامل این موارد باشد: تابع، نام ستون غیر محاسباتی و مقادیر ثابت ولی امکان استفاده از subquery وجود ندارد.
ستون‌های محاسباتی بطور پیشفرض مجازی هستند (بطور فیزیکی بر روی دیسک ذخیره نشده اند). یعنی هر موقع که query اجرا می‌شود آنها نیز مجدد محاسبه شده و نمایش داده می‌شوند.
برای اینکه نوع ذخیره سازی را از مجازی به فیزیکی تبدیل کنیم باید در هنگام ساخت جدول (یا تغییر آن) از کلید واژه PERSISTED استفاده کنیم. وقتی بطور فیزیکی ذخیره شده باشد با هر بار ویرایش یکی از ستون‌های تشکلیل دهنده ستون محاسباتی هم ویرایش می‌شود.
ستون محاسباتی بعد از تبدیل شدن از مجازی به فیزیکی می‌تواند به عنوان کلید اولیه و ایندکس در نظر گرفته شود.

به مثال زیر توجه کنید:
جدولی داریم با دو ستون، قرار هست بر اساس ترکیب مقادیر دو ستون جستجویی انجام دهیم. ضمن اینکه ترکیب دو ستون باید منحصر بفرد باشد. برای این منظور یک unique index روی دو ستون لحاظ می‌کنیم.
create table t1
(
col1 char(1),
col2 char(1)
)

create unique nonclustered index ix_uq on t1 (col1 , col2);

insert t1 
values('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E'), ('E', 'F'),
('G', 'H'), ('I', 'J'), ('K', 'L'), ('M', 'N'), ('O', 'P');

اکنون به دنبال سطری میگردیم که ترکیب مقادیر دو ستون آن برابر با OP باشد. پس query زیر را اجرا میکنیم
select col1 + col2
from t1
where col1 + col2 = 'OP'
اما همانطور که در تصویر زیر مشاهده می‌شود عمل Index Seek صورت نگرفته است. زمانی که از ستون به عنوان یک عبارت استفاده شود Index Seek نخواهیم داشت. منظور عبارت، الحاق مقداری با ستون، قرار گرفتن ستون در یک تابع و ... می‌باشد.


برای اینکه Index Seek داشته باشیم بایستی مقادیر را جداگانه مقایسه کنیم(ستون‌ها به صورت عبارت محاسباتی نباشند)
select col1 + col2
from t1
where col1  = 'O' and col2 = 'P'

ولی ما می‌خواهیم شرط بر اساس ترکیب دو ستون باشد. خب اینجا هست که Computed Columns مطرح میشوند.
alter table t1 add col3 as col1 + col2 persisted

create clustered index ix1 on t1 (col3)
با دستور اول یک ستون محاسباتی از نوع persisted به جدول اضافه نمودیم. و با دستور دوم یک Index روی ستون محاسباتی ایجاد نمودیم.
حال مجددا عمل جستجو را انجام میدهیم ولی به کمک ستون محاسباتی که اخیرا ایجاد نمودیم:
select *
from t1
where col3 = 'OP';


حالا مشاهده می‌شود که شاخص ix1 اسکن نشده است. و از آنجایی که شاخص از نوع Clustered است مشکل Covering هم نخواهیم داشت.


مطالب
استفاده از MembershipProvider و RoleProvider در windows Application
برای استفاده از  سیستم مدیریت کاربران و نقش‌های آنها به یک پیاده سازی از کلاس انتزاعی MembershipProvider نیاز داریم. SQL Membership Provider تو کار دات نت، انتخاب پیش فرض ماست ولی به دلیل طراحی در دات نت 2 و نیاز سنجی قدیمی اون و همچنین گره زدن برنامه با sql server  (استفاده از stored procedure و... ) انتخاب مناسبی نیست. پیشنهاد خود مایکروسافت استفاده از SimpleMembership است که این پیاده سازی قابلیت‌های بیشتری از MembershipProvider پایه رو دارد. این قابلیت‌های بیشتر با استفاده از کلاس انتزاعی ExtendedMembershipProvider که خود از از MembershipProvider مشتق شده است میسر شده است.
برای این آموزش ما از SimpleMembership  استفاده می‌کنیم اگر  شما دوست ندارید از SimpleMembership  استفاده کنید می‌تونید از Provider  های دیگه ای استفاده کنید و حتی می‌تونید یک پروایدر سفارشی برای خودتون بنویسید.
برای شروع یک پروژه ConsoleApplication  تعریف کنید و رفرنس‌های زیر رو اضافه کنید.
System.Web.dll 
System.Web.ApplicationServices.dll
 خاصیت  Copy Local دو کتابخانه زیر رو true  ست کنید.
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies\WebMatrix.Data.dll
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies\WebMatrix.WebData.dll
- در صورتیکه یک پروژه Asp.Net MVC 4 به همراه تمپلت Internet Application بسازید بصورت خودکار SimpleMembership  و رفرنس‌های آن به پروژه اضافه می‌شود.
 یک فایل App.config با محتویات زیر به پروژه اضافه کنید  و تنظیمات ConnectionString را مطابق با دیتابیس موجود در کامپیوتر خود تنظیم کنید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <connectionStrings>
    <add name="DefaultConnection" 
         connectionString="Data Source=.;Initial Catalog=SimpleMembershipProviderDB;Integrated Security=True;"
         providerName="System.Data.SqlClient"/>
  </connectionStrings>
  
  <system.web>
    <roleManager enabled="true" defaultProvider="SimpleRoleProvider">
      <providers>
        <clear/>
        <add name="SimpleRoleProvider" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData"/>
      </providers>
    </roleManager>
    <membership defaultProvider="SimpleMembershipProvider">
      <providers>
        <clear/>
        <add name="SimpleMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData"/>
      </providers>
    </membership>

  </system.web>
</configuration>
محتویات فایل Program.cs :
using System; 
using System.Security.Principal; 
using System.Web.Security;
using WebMatrix.WebData;

namespace MemberShipConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            WebSecurity.InitializeDatabaseConnection("DefaultConnection",
                "UserProfile", "UserId", "UserName", 
                autoCreateTables: true);

            AddUserAndRolSample();
            Login();
            if (System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated)
                RunApp();

        }

        static void AddUserAndRolSample()
        {
            if (WebSecurity.UserExists("iman"))
                return;

            //  No implements in SimpleMembershipProvider :
            //     Membership.CreateUser("iman", "123");
            WebSecurity.CreateUserAndAccount("iman", "123");
            Roles.CreateRole("admin");
            Roles.CreateRole("User");
            Roles.AddUserToRole("iman", "admin");
        }

        static void Login()
        {
            for (int i = 0; i < 3; i++)
            {
                Console.Write("UserName: ");
                var userName = Console.ReadLine();
                Console.Write("Password: ");
                var password = Console.ReadLine();
                if (Membership.ValidateUser(userName, password))
                {

                    var user = Membership.GetUser(userName);
                    var identity = new GenericIdentity(user.UserName);
                    var principal = new RolePrincipal(identity);
                    System.Threading.Thread.CurrentPrincipal = principal;

                    Console.Clear();
                    return;
                }


                Console.WriteLine("User Name Or Password Not Valid.");
            }

        }
        static void RunApp()
        {
            Console.WriteLine("Welcome To MemberShip. User Name: {0}",
                System.Threading.Thread.CurrentPrincipal.Identity.Name);

            if (System.Threading.Thread.CurrentPrincipal.IsInRole("admin"))
                Console.WriteLine("Hello Admin User!");

            Console.Read();
        }
    }
}
در ابتدا با فراخوانی متد InitializeDatabaseConnection  تنظیمات اولیه simpleMembership را مقدار دهی می‌کنیم. این متد حتما باید یکبار اجرا شود.
InitializeDatabaseConnection(string connectionStringName, 
                             string userTableName, 
                             string userIdColumn, 
                             string userNameColumn, 
                             bool autoCreateTables)                            
ملاحظه می‌کنید پارامتر آخر متد مربوط به ساخت جداول مورد نیاز این پروایدر است. در صورتی که بخواهیم در پروژه از EntityFramework استفاده کنیم می‌تونیم موجودیت‌های معادل جدول‌های مورد نیاز SimpleMembership  رو در EF  بسازیم و در این متد AutoCreateTables رو False مقدار دهی کنیم. برای بدست آوردن موجودیت‌های معادل جدول‌های این پروایدر با ابزار Entity Framework Power Tools و روش مهندسی معکوس ، تمام موجودیت‌های یک دیتابیس ساخته شده رو استخراج می‌کنیم.
 - SimpleMembership  از تمام خانواده microsoft sql پشتبانی می‌کنه (SQL Server, SQL Azure, SQL Server CE, SQL Server Express,LocalD)   برای سایر دیتابیس‌ها به علت تفاوت در دستورات ساخت تیبل‌ها و ... مایکروسافت تضمینی نداده ولی اگر خودمون جدول‌های مورد نیاز SimpleMembership  رو ساخته باشیم احتمالا در سایر دیتابیس‌ها هم قابل استفاده است.
 در ادامه برنامه بالا یک کاربر و دو نقش تعریف کردیم و نقش admin  رو به کاربر نسبت دادیم. در متد login در صورت معتبر بودن کاربر  ، اون رو به ترد اصلی برنامه معرفی می‌کنیم. هر جا خواستیم می‌تونیم نقشهای کاربر رو چک کنیم و نکته آخر با اولین چک کردن نقش یک کاربر تمام نقش‌های اون در حافظه سیستم کش می‌شود و تنها مرتبه اول با دیتابیس ارتباط برقرار می‌کند.

مطالب
SQL Instance
ممکن است کاربر بر روی سیستم خود نسخه‌های مختلفی از SQL Server را نصب کرده باشد. برای مثال SQL Express, SQL 2005, SQL 2008. و یا نسخه ای خاص (مثلا 2012) را چند بار روی سیستم خود نصب کرده باشد. SQL برای تفکیک این نسخه‌ها و نصب‌ها از مفهومی با عنوان Instance استفاده می‌کند. یعنی به هر نسخه نصب شده نامی یکتا می‌دهد تا بتوان به تفکیک به آنها دسترسی داشت.
برای اتصال به این نسخه‌ها باید در بخش آدرس سرور، از ترکیب نام سیستم و نام Instance به این شکل استفاده کرد:  SystemName\Instance
بعضی مواقع لازم است که لیست Instance‌های نصب شده روی سیستم کاربر را به دست آوریم. ADO.NET کلاسی به همین منظور تعبیه کرده که شبکه را جستجو کرده و SQL Instance‌های مختلف را که قابل دسترسی هستند را برای شما لیست می‌کند. استفاده از این کلاس بسیار ساده است:
using System.Data.Sql;

class Program
{
  static void Main()
  {
    // Retrieve the enumerator instance and then the data.
    SqlDataSourceEnumerator instance =
      SqlDataSourceEnumerator.Instance;
    System.Data.DataTable table = instance.GetDataSources();

    // Display the contents of the table.
    DisplayData(table);

    Console.WriteLine("Press any key to continue.");
    Console.ReadKey();
  }

  private static void DisplayData(System.Data.DataTable table)
  {
    foreach (System.Data.DataRow row in table.Rows)
    {
      foreach (System.Data.DataColumn col in table.Columns)
      {
        Console.WriteLine("{0} = {1}", col.ColumnName, row[col]);
      }
      Console.WriteLine("============================");
    }
  }
}

البته با توجه به اینکه شبکه را جستجو می‌کند در نرم افزار شما وقفه خواهد انداخت. خوب اگه بخواهیم Instance‌های نصب شده روی سیستم کاربر را پیدا کنیم چی؟ ساده‌ترین و سریعترین راه استفاده از رجیستری سیستم است. نام Instance‌ها در رجیستری ویندوز در آدرس زیر قابل دسترسی است:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names

برای استفاده از این کلید در c# می‌توان از کد زیر کمک بگیرید:
            var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names");

            foreach (string sk in key.GetSubKeyNames())
            {
                var rkey = key.OpenSubKey(sk);
                foreach (string s in rkey.GetValueNames())
                {
                    MessageBox.Show("Sql instance name:" + s);
                }
            }
فقط دو نکته قابل توجه است. برنامه باید در Any CPU کامپایل شود تا در سیستم‌های 64 بیتی بتوانید به محل درست رجیستری دسترسی پیدا کنید. چون نرم افزارهای 32 بیت در ویندوز 64 بیت در سیستم wow64 اجرا می‌شود که دسترسی به رجیستری آن در آدرس wow64 هر قسمت رجیستری است. بنابراین کد فوق در حالت Any CPU و غیر فعال بودن Prefer 32-bit قسمت Build در Properties برنامه به درستی اجرا می‌شود.
نکته: Default Instance در SQL مقدار MSSQLSERVER  می‌باشد.
نظرات مطالب
بازنویسی سطح دوم کش برای Entity framework 6
تابع InvalidateCacheDependencies  فراخوانی می‌شود و مقدار changedEntityNames برابر string[0] می‌باشد.  کلاس جدید  ChangeTrackerExtenstions   را اعمال کردم در زمان اجرا با خطای زیر مواجه شدم. 
 Trace.WriteLine(string.Format("Changed Entity Names: {0}", changedEntityNames.Aggregate((e1, e2) => string.Format("{0}, {1}", e1, e2))))
اشتراک‌ها
انتشار کتاب Entity Framework 6 Recipes

مدل سازی مفهومی بارزترین ویژگی Entity Framework است و اصل این کتاب را تشکیل می‌دهد. Entity Framework بر اساس الگوهای دسترسی به داده قبلی ساخته شده است و محیطی را فراهم کرده است که از مدل سازی در سطح domain واقعی پشتیبانی می‌کند.

Entity Framework 6 تبدیل به تکنولوژی دسترسی به داده بالغ و پخته ای شده است که با امکانات زیاد خود، آماده‌ی استفاده در برنامه‌های جدید و قدیمی است. 

انتشار کتاب Entity Framework 6 Recipes
مطالب
پشتیبانی از حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0
همواره حذف و به روز رسانی تعداد زیادی رکورد توسط EF، بسیار غیربهینه و کند بوده‌است؛ از این جهت که یکی از روش‌های انجام اینکار، کوئری گرفتن از رکوردهای مدنظر جهت حذف، سپس بارگذاری آن‌ها در حافظه و در آخر حذف یکی یکی آن‌ها بوده‌است:
using var dbContext = new MyDbContext();
var objectToDelete = await dbContext.Objects.FirstAsync(o => o.Id == id);
dbContext.Objects.Remove(objectToDelete);
await dbContext.SaveChangesAsync();
در اینجا در ابتدا، شیء‌ای که قرار است حذف شود، از بانک اطلاعاتی کوئری گرفته می‌شود تا وارد سیستم Change Tracking شود. سپس از این سیستم ردیابی اطلاعات درون حافظه‌ای، حذف خواهد شد و در نهایت این تغییرات به بانک اطلاعاتی اعمال می‌شوند. بنابراین در این مثال ساده، حداقل دوبار رفت و برگشت به بانک اطلاعاتی وجود خواهد داشت.
البته راه دومی نیز برای انجام اینکار وجود دارد:
using var dbContext = new MyDbContext();
var objectToDelete = new MyObject { Id = id };
dbContext.Objects.Remove(objectToDelete);
await dbContext.SaveChangesAsync();
در این مثال، رفت و برگشت ابتدایی، حذف شده‌است و با فرض معلوم بودن کلید اصلی رکورد مدنظر، آن‌را وارد سیستم Change Tracking کرده و درنهایت آن‌را حذف می‌کنیم. کار متد Remove در اینجا، علامتگذاری این شیء دارای Id، به صورت EntityState.Deleted است.

اکنون می‌توان در EF 7.0، روش سومی را نیز به این لیست اضافه کرد که فقط یکبار رفت و برگشت به بانک اطلاعاتی را سبب می‌شود:
await dbContext.Objects.Where(x => x.Id == id).ExecuteDeleteAsync();


معرفی متدهای حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0

EF 7.0 به همراه دو متد جدید ExecuteUpdate و ExecuteDelete (و همچنین نگارش‌های async آن‌ها) است که کار به‌روز رسانی و یا حذف دسته‌ای رکوردها را بدون دخالت سیستم Change tacking میسر می‌کنند. مزیت مهم این روش، عدم نیاز به کوئری گرفتن از بانک اطلاعاتی جهت بارگذاری رکوردهای مدنظر در حافظه و سپس حذف یکی یکی آن‌ها است. فقط باید دقت داشت که چون این روش خارج از سیستم Change tracking صورت می‌گیرد، نتیجه‌ی حاصل، دیگر با اطلاعات درون حافظه‌ای سمت کلاینت، هماهنگ نخواهد بود و کار به روز رسانی دستی آن‌ها به‌عهده‌ی شماست.


بررسی نحوه‌ی عملکرد ExecuteUpdate و ExecuteDelete با یک مثال

فرض کنید مدل‌های موجودیت‌های برنامه شامل کلاس‌های زیر هستند:
public class User
{
    public int Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public virtual List<Book> Books { get; set; } = new();
    public virtual Address? Address { get; set; }
}

public class Book
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required string Name { get; set; }

    public virtual User User { get; set; } = default!;
    public int UserId { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public required string Street { get; set; }
    
    public virtual User User { get; set; } = default!;
    public int UserId { get; set; }
}
که در اینجا یک کاربر می‌تواند دارای یک آدرس و چندین کتاب تعریف شده باشد؛ با این Context ابتدایی:
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; } = default!;

    public DbSet<Book> Books { get; set; } = default!;

    public DbSet<Address> Addresses { get; set; } = default!;
}

مثال 1: حذف دسته‌ای تعدادی کتاب
context.Books.Where(book => book.Name.Contains("1")).ExecuteDelete();
در اینجا نحوه‌ی استفاده از متد ExecuteDelete را مشاهده می‌کنید که به انتهای LINQ Query، اضافه شده‌است. در این مثال، تمام کتاب‌هایی که در نامشان حرف 1 وجود دارد، حذف می‌شوند. این کوئری، به صورت زیر بر روی بانک اطلاعاتی اجرا می‌شود:
DELETE FROM [b]
FROM [Books] AS [b]
WHERE [b].[Name] LIKE N'%1%'
مهم‌ترین مزیت این روش، عدم نیاز به بارگذاری و یا ساخت درون حافظه‌ای لیست کتاب‌هایی است که قرار است حذف شوند. کل این عملیات در یک رفت و برگشت ساده و سریع انجام می‌شود.

یک نکته: متد ExecuteDelete، تعداد رکوردهای حذف شده را نیز بازگشت می‌دهد.


مثال 2: حذف کاربران و تمام رکوردهای وابسته به آن

فرض کنید می‌خواهیم تعدادی از کاربران را از بانک اطلاعاتی حذف کنیم:
context.Users.Where(user => user.Id <= 500).ExecuteDelete();
اگر این کوئری را با تنظیمات فعلی اجرا کنیم، با خطای زیر متوقف خواهیم شد:
DELETE FROM [u]
FROM [Users] AS [u]
WHERE [u].[Id] <= 500

The DELETE statement conflicted with the REFERENCE constraint "FK_Books_Users_UserId".
The conflict occurred in database "EF7BulkOperations", table "dbo.Books", column 'UserId'.
عنوان می‌کند که یک کاربر، دارای تعدادی کتاب و آدرسی از پیش ثبت شده‌است و نمی‌توان آن‌را بدون حذف وابستگی‌های آن، حذف کرد. اگر کاربری را حذف کنیم، کلید‌های خارجی ذکر شده‌ی در جداولی که این کلید خارجی را به همراه دارند، غیرمعتبر می‌شوند (و این کلید خارجی تعریف شده، نال پذیر هم نیست). برای رفع این مشکل، یا باید ابتدا در طی دستوراتی جداگانه، وابستگی‌های ممکن را حذف کنیم و یا می‌توان تنظیم cascade delete را به نحو زیر به تعریف جداول مرتبط اضافه کرد تا صدور یک دستور delete، به صورت خودکار سبب حذف وابستگی‌های مرتبط نیز شود:
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; } = default!;

    public DbSet<Book> Books { get; set; } = default!;

    public DbSet<Address> Addresses { get; set; } = default!;

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

        modelBuilder
            .Entity<User>()
            .HasMany(user => user.Books)
            .WithOne(book => book.User)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder
            .Entity<User>()
            .HasOne(user => user.Address)
            .WithOne(address => address.User)
            .HasForeignKey<Address>(address => address.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
همانطور که ملاحظه می‌کنید، به متد OnModelCreating تنظیم cascade delete وابستگی‌های جدول کاربران اضافه شده‌است. پس از این تنظیم، دستور مثال دوم، بدون مشکل اجرا شده و حذف یک کاربر، سبب حذف خودکار کتاب‌ها و آدرس او نیز می‌شود.


مثال 3: به‌روز رسانی دسته‌ای از کاربران

فرض کنید می‌خواهیم LastName تعدادی کاربر مشخص را به مقدار جدید Updated، تغییر دهیم:
context.Users.Where(user => user.Id <= 400)
   .ExecuteUpdate(p => p.SetProperty(user => user.LastName,  user => "Updated"));
برای اینکار، پس از مشخص شدن شرط کوئری در قسمت Where، کار به روز رسانی توسط متد ExecuteUpdate و سپس متد SetProperty صورت می‌گیرد. در اینجا در ابتدا مشخص می‌کنیم که کدام خاصیت قرار است به روز رسانی شود و پارامتر دوم آن، مقدار جدید را مشخص می‌کند. این کوئری به نحو زیر به بانک اطلاعاتی اعمال خواهد شد:
UPDATE [u]
SET [u].[LastName] = N'Updated'
FROM [Users] AS [u]
WHERE [u].[Id] <= 400
در اینجا می‌توان در پارامتر دوم متد SetProperty، از مقدار فعلی سایر خواص نیز استفاده کرد:
context.Users.Where(user => user.Id <= 300)
  .ExecuteUpdate(p => p.SetProperty(user => user.LastName,
      user => "Updated" + user.LastName));
که خروجی زیر را تولید می‌کند:
UPDATE [u]
SET [u].[LastName] = N'Updated' + [u].[LastName]
FROM [Users] AS [u]
WHERE [u].[Id] <= 300
همچنین می‌توان چندین متد SetProperty را نیز به صورت زنجیروار، جهت به روز رسانی چندین خاصیت و فیلد، ذکر کرد:
context.Users.Where(user => user.Id <= 800)
   .ExecuteUpdate(p => p.SetProperty(user => user.LastName,
        user => "Updated" + user.LastName)
             .SetProperty(user => user.FirstName,
                 user => "Updated" + user.FirstName));
با این خروجی نهایی:
UPDATE [u]
SET [u].[FirstName] = N'Updated' + [u].[FirstName],
[u].[LastName] = N'Updated' + [u].[LastName]
FROM [Users] AS [u]
WHERE [u].[Id] <= 800
متد ExecuteUpdate، تعداد رکوردهای به‌روز رسانی شده را نیز بازگشت می‌دهد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید:  EF7BulkOperations.zip
اشتراک‌ها
Entity Framework Core 5.0 Preview 2 منتشر شد

The previews of EF Core 5.0 require .NET Standard 2.1. This means:

  • EF Core 5.0 runs on .NET Core 3.1; it does not require .NET 5.
    • This may change in future previews depending on how the plan for .NET 5 evolves.
  • EF Core 5.0 runs on other platforms that support .NET Standard 2.1.
  • EF Core 5.0 will not run on .NET Standard 2.0 platforms, including .NET Framework. 
Entity Framework Core 5.0 Preview 2 منتشر شد
مطالب
گروه بندی اطلاعات و گزارشات Master-Details در PdfReport
اگر به بانک اطلاعاتی مثال‌های همراه سورس‌های PdfReport در مسیر Bin\Data\blogs.sqlite مراجعه کنید، دو جدول والدین و فرزندان هم در آن وجود دارند:



بر این اساس قصد داریم رابطه یک به چند فوق را گروه بندی شده نمایش دهیم:


(البته این اعداد و اطلاعات، به صورت اتفاقی تولید شده‌اند و الزامی ندارد که والد متولد 2002 هنوز والد شده باشد؛ یا اینکه فرزندی متولد 2003 داشته باشد!)

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

سورس کامل این مثال را در ادامه مشاهده می‌کنید:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.MasterDetails
{
    public class MasterDetailsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(new MasterDetailsHeaders { PdfRptFont = header.PdfFont });
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .MainTableTemplate(t => t.BasicTemplate(BasicTemplate.SilverTemplate))
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = false,
                    SpacingBeforeAllGroupsSummary = 5f,
                    NewGroupAvailableSpacingThreshold = 170
                });
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.GenericDataReader(
                   providerName: "System.Data.SQLite",
                   connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                   sql: @"select 
                            tblParents.BirthDate as ParentBirthDate,
                            tblParents.Name as ParentName,
                            tblParents.LastName as ParentLastName,
                            tblKids.Name as KidName,
                            tblKids.BirthDate as KidBirthDate
                            from tblParents
                                left outer join tblKids
                                     on tblKids.ParentId = tblParents.Id
                            order by 
                                tblParents.Name,
                                tblParents.LastName,
                                tblKids.Name"
               );
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Left);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentBirthDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("ParentBirthDate");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        var date1 = (DateTime)val1;
                        var date2 = (DateTime)val2;
                        return date1.Year == date2.Year && date1.Month == date2.Month && date1.Day == date2.Day;
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("ParentName");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentLastName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("ParentLastName");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("KidName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("Child Name");
                    column.IsVisible(true);
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("KidBirthDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("BirthDate");
                    column.IsVisible(true);
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(e => e.ToExcel())
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptMasterDetailsSample.pdf"));
        }
    }
}
به همراه سر ستون‌های مجزای هر گروه و صفحه:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.ColumnsItemsTemplates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.MasterDetails
{
    public class MasterDetailsHeaders : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }

        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
            var parentName = newGroupInfo.GetSafeStringValueOf("ParentName");
            var parentLastName = newGroupInfo.GetSafeStringValueOf("ParentLastName");
            var parentBirthDate = newGroupInfo.GetSafeStringValueOf("ParentBirthDate");

            var table = new PdfPTable(relativeWidths: new[] { 1f, 5f }) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Last Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentLastName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.Value = "Birth Date:";
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
               },
               (cellData, cellProperties) =>
               {
                   cellData.Value = parentBirthDate;
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
               });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 5f);
        }

        public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            var table = new PdfPTable(numColumns: 1) { WidthPercentage = 100 };
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.CellTemplate = new ImageFilePathField();
                   cellData.Value = AppPath.ApplicationPath + "\\Images\\01.png";
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
               });
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.Value = "Family rpt";
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
               });
            return table.AddBorderToTable();
        }
    }
}
توضیحات:
- منبع داده مورد استفاده در اینجا از نوع GenericDataReader است؛ جهت خواندن رکوردهای بانک اطلاعاتی SQLite ذکر شده در ابتدای بحث. (دو مثال دیگر هم به پوشه مثال‌های سورس‌های PdfReport اضافه شده‌اند به نام‌های Grouping و WrapGroupsInColumns که به همین موضوع گروه بندی می‌پردازند؛ البته با استفاده از StronglyTypedList‌ها. ولی درکل مفاهیم و اصول آن‌ها یکی است.)
select 
          tblParents.BirthDate as ParentBirthDate,
          tblParents.Name as ParentName,
          tblParents.LastName as ParentLastName,
          tblKids.Name as KidName,
          tblKids.BirthDate as KidBirthDate
          from tblParents
                  left outer join tblKids
                        on tblKids.ParentId = tblParents.Id
                            order by 
                                tblParents.Name,
                                tblParents.LastName,
                                tblKids.Name
در کوئری فوق (و کلا گروه بندی اطلاعات) دو نکته حائز اهمیت است:
الف) چون قرار است اطلاعات بر اساس مشخصات والدین و فرزندان آن‌ها گروه بندی شود، نیاز است حتما order by  و مرتب سازی رکوردها قید گردد.
ب) در PdfReport نمی‌توانید در خواص معرفی شده جهت تعریف ستون‌ها، از نام‌های تکراری استفاده کنید. برای رفع این مشکل استفاده از Alias پیشنهاد می‌شود؛ مانند:
tblParents.Name as ParentName,
tblKids.Name as KidName,
- مشخص سازی خاصیت و ستونی که قرار است در گروه بندی شرکت کند بسیار ساده است:
column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
در اینجا به کمک متد Group، قابلیت گروه بندی بر روی این ستون فعال شده و سپس باید فرمولی را جهت مشخص سازی حد و مرز گروه مشخص کنیم. برای مثال در اینجا اگر مقادیر ردیف جاری (val2) و ردیف قبلی (val1) یکسان نبودند، یعنی گروه خاتمه یافته و گروه جدیدی شروع می‌شود (به همین جهت عنوان شد که مرتب سازی اطلاعات ضروری است).

- تنظیم دیگری را که در اینجا می‌توان ذکر کرد، مورد ذیل است:
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = false,
                    SpacingBeforeAllGroupsSummary = 5f,
                    NewGroupAvailableSpacingThreshold = 170
                });
به این ترتیب می‌توان مشخص کرد که آیا باید ستون‌های دخیل در گروه بندی، در گزارش نمایش داده شوند یا خیر (GroupType.HideGroupingColumns)، آیا سر ستون هر جدول، به ازای هر گروه باید تکرار شود؟ (RepeatHeaderRowPerGroup)، آیا در هر صفحه یک گروه نمایش داده شود (ShowOneGroupPerPage) یا اینکه گروه‌ها به صورت متوالی در صفحات درج شوند. توسط SpacingBeforeAllGroupsSummary، فاصله جمع نهایی تمام گروه‌ها از آخرین گروه نمایش داده شده مشخص می‌شود. به کمک NewGroupAvailableSpacingThreshold مشخص می‌کنیم که در چه فاصله‌ای از انتهای صفحه، گروه جدیدی نباید درج شود و این گروه باید به صفحه بعدی منتقل شده و از آنجا شروع شود.

- اگر به تصویر ابتدای مطلب دقت کرده باشید، علاوه بر هدر صفحه، هر گروه نیز یک هدر مجزا دارد. برای طراحی آن باید اینترفیس IPageHeader را پیاده سازی کرد که نمونه‌ای از آن‌را در کلاس MasterDetailsHeaders فوق مشاهده می‌کنید.
        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
            var parentName = newGroupInfo.GetSafeStringValueOf("ParentName");
            var parentLastName = newGroupInfo.GetSafeStringValueOf("ParentLastName");
            var parentBirthDate = newGroupInfo.GetSafeStringValueOf("ParentBirthDate");

            var table = new PdfPTable(relativeWidths: new[] { 1f, 5f }) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
ساختار آن هم بسیار ساده است. توسط  newGroupInfo می‌توان به اطلاعات گروه جدید، دسترسی یافت. برای مثال در اینجا اطلاعات والد گروه جدید در حال تهیه، دریافت شده و سپس در ردیف‌های یک جدول دو ستونه درج می‌شود. در ستون اول آن یک برچسب و در ستون دوم، مقدار دریافتی نمایش داده شده است و همینطور الی آخر برای سایر ردیف‌ها.
مطالب
کوئری نویسی در EF Core - قسمت سوم - جوین نویسی
پس از آشنایی با نوشتن یک سری کوئری‌های ساده در EF Core، در این قسمت به نحوه‌ی گزارشگیری از اطلاعات چندین جدول مرتبط به هم توسط Joinها خواهیم پرداخت.

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

چگونه می‌توان زمان‌های شروع رزروهای کاربری به نام «David Farrell» را یافت؟


همانطور که در دیاگرام فوق مشاهده می‌کنید، به ازای هر ID کاربری در جدول کاربران، به دنبال ردیف‌هایی در جدول Bookings هستیم که این ID در آن‌ها درج شده‌است. اما ... در EF-Core برخلاف SQL نویسی معمولی، ما کاری به ذکر قسمت اتصالی ON [Bookings].[MemId] = [Members].[MemId] نداریم. همینقدر که در کوئری نوشته شده به یک سر دیگر رابطه و خاصیت راهبری (navigation property) دیگری اشاره شود، خود EF-Core جوینی را به صورت خودکار تشکیل خواهد داد و شرط یاد شده را نیز برقرار می‌کند.
در قسمت اول این سری، در حین طراحی موجودیت کاربر، برای تشکیل سر دیگر رابطه‌ی one-to-many آن، به جدول Bookings، خاصیت Member را نیز که بیانگر کلید خارجی به جدول کاربران است، اضافه کردیم:
namespace EFCorePgExercises.Entities
{
    public class Booking
    {
       // ...

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

       // ...
    }
}
خاصیت عددی MemId، کلید خارجی است که در بانک اطلاعاتی رابطه‌ای ثبت خواهد شد و خاصیت Member، خاصیت راهبری است که جوین نویسی به جدول کاربران را بدون ذکر صریح جوین میسر می‌کند:
var startTimes = context.Bookings
                        .Where(booking => booking.Member.FirstName == "David"
                                            && booking.Member.Surname == "Farrell")
                        .Select(booking => new { booking.StartTime })
                        .ToList();
در این کوئری همینقدر که در قسمت Where آن booking.Member ذکر شده، جوینی به جدول کاربران را به صورت خودکار تشکیل می‌دهد:




مثال 2: یافتن زمان‌های شروع به رزرو شدن یک امکان خاص در مجموعه.
لیست زمان‌های شروع به رزرو شدن زمین(های) تنیس را برای روز 2012-09-21 تولید کنید. خروجی آن باید به همراه ستون‌های StartTime, FacilityName باشد.

طراحی موجودیت Booking، به همراه یک کلید خارجی به Facility نیز هست:
namespace EFCorePgExercises.Entities
{
    public class Booking
    {
       // ...

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

       // ...
    }
}
خاصیت عددی FacId، کلید خارجی Facility است که در بانک اطلاعاتی رابطه‌ای ثبت خواهد شد و خاصیت Facility، خاصیت راهبری است که جوین نویسی به جدول Facilities را بدون ذکر صریح جوین میسر می‌کند:
int[] tennisCourts = { 0, 1 };
var date1 = new DateTime(2012, 09, 21);
var date2 = new DateTime(2012, 09, 22);
var startTimes = context.Bookings
                        .Where(booking => tennisCourts.Contains(booking.Facility.FacId)
                                && booking.StartTime >= date1
                                && booking.StartTime < date2)
                        .Select(booking => new { booking.StartTime, booking.Facility.Name })
                        .ToList();
- زمین‌های تنیس این مجموعه، دارای دو Id مساوی 0 و 1 هستند که در اینجا به صورت صریحی مشخص شده‌اند تا مانند مثال 6 قسمت قبل عمل شود. روش دیگر یافتن آن‌ها می‌تواند مانند مثال 5 قسمت قبل باشد که به صورت «Name.Contains("Tennis")» نوشته شد.
- در قسمت Where این کوئری چون booking.Facility ذکر شده، سبب ایجاد جوین خودکاری به جدول Facilities خواهد شد.
- علت استفاده‌ی از دو تاریخ در اینجا برای یافتن اطلاعات تنها یک روز، ثبت زمان، به همراه تاریخ رزرو است. ستون تاریخ شروع، به صورت «2012-09-21 18:00:00.0000000» مقدار دهی شده‌است و نه به صورت «2012-09-21». البته در EF-Core راه دیگری هم برای حل این مساله وجود دارد. هر خاصیت از نوع DateTime، به همراه خاصیت Date نیز هست. برای مثال اگر بجای booking.StartTime نوشته شود booking.StartTime.Date (به خاصیت Date اضافه شده دقت کنید)، کد SQL حاصل، به همراه «CONVERT(date, [b].[StartTime])» خواهد بود که سبب حذف خودکار قسمت زمان این ستون می‌شود.



مثال 3: تولید لیست کاربرانی که کاربر دیگری را توصیه کرده‌اند.

چگونه می‌توان لیست کاربرانی را یافت که کاربر دیگری را توصیه کرده‌اند؟ این لیست نباید به همراه ردیف‌های تکراری باشد و همچنین باید بر اساس surname, firstname مرتب شود.

در اینجا به مفهوم جوین کردن یک جدول با خودش رسیده‌ایم. جدول کاربران، یک جدول خود ارجاع دهنده‌است:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

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

       // ...
    }
}
که در اینجا RecommendedBy، یک کلید خارجی نال پذیر است که به Id همین جدول اشاره می‌کند. دو خاصیت دیگر تعریف شده، مکمل این خاصیت عددی، جهت سهولت کوئری نویسی‌های EF-Core هستند. برای مثال اگر در کوئری Recommender != null ذکر شود، سبب تشکیل جوینی به همین جدول شده و لیست کاربرانی را ارائه می‌دهد که کاربر دیگری را توصیه کرده‌اند:
var members = context.Members
                        .Where(member => member.Recommender != null)
                        .Select(member => new { member.Recommender.FirstName, member.Recommender.Surname })
                        .Distinct()
                        .OrderBy(member => member.Surname).ThenBy(member => member.FirstName)
                        .ToList();
وجود Distinct سبب بازگشت ردیف‌هایی غیرتکراری می‌شود (چون دو خاصیت نام و نام خانوادگی انتخاب شده‌اند، ردیف غیرتکراری، ردیفی خواهد بود که هر دوی این ستون‌ها در آن وجود نداشته باشد) و روش مرتب سازی بر اساس دو خاصیت را نیز مشاهده می‌کنید. در اینجا نباید دوبار OrderBy را پشت سر هم ذکر کرد. بار اول OrderBy است و بار دوم ThenBy تعریف می‌شود:



مثال 4: تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها.

چگونه می‌توان لیست کاربران را به همراه توصیه کننده‌ی آن‌ها تولید کرد؟ این لیست باید بر اساس surname, firstname مرتب شود.
var members = context.Members
                        .Select(member => new
                        {
                            memFName = member.FirstName,
                            memSName = member.Surname,
                            recFName = member.Recommender.FirstName ?? "",
                            recSName = member.Recommender.Surname ?? ""
                        })
                        .OrderBy(member => member.memSName).ThenBy(member => member.memFName)
                        .ToList();
در اینجا نیز می‌توان با ذکر member.Recommender سبب تولید یک جوین خودکار شد. همچنین همانطور که در مثال 7 قسمت قبل نیز بررسی کردیم، می‌توان بر روی خواص ذکر شده‌ی در Select، محاسباتی را نیز انجام داد. برای مثال در اینجا بجای درج مقدار null برای کاربرانی که کاربر دیگری را توصیه نکرده‌اند، ترجیح داده‌ایم که یک رشته‌ی خالی بازگشت داده شود که به صورت «COALESCE ([m0].[FirstName], N'')» ترجمه می‌شود:


همانطور که ملاحظه می‌کنید، نوع جوین خودکار تشکیل شده، Left join است و دیگر مانند جوین‌های مثال‌های ابتدای بحث، inner join نیست. در inner join، جدول سمت راست و چپ بر اساس شرط ON آن‌ها با هم مقایسه شده و ردیف‌های کاملا تطابق یافته‌ای بازگشت داده می‌شوند. کار Left join نیز مشابه است، با این تفاوت که در اینجا ممکن است برای جدول سمت چپ، هیچ ردیف تطابق یافته‌ای در جدول سمت راست وجود نداشته باشد (نوع آن بر اساس نال پذیری خاصیت RecommendedBy تشخیص داده شده‌است)؛ برای مثال یک کاربر ممکن است توسط کاربر دیگری توصیه نشده باشد (و RecommendedBy او نال باشد)، اما علاقمندیم که نام او در لیست نهایی حضور داشته باشد و حذف نشود.


یک نکته: در SQL Server تفاوتی بین left join و left outer join وجود ندارد و ذکر واژه‌ی کلیدی outer کاملا اختیاری است. جدول موارد مشابهی در SQL Server که به یک معنا هستند، صورت زیر است:
A LEFT JOIN B            A LEFT OUTER JOIN B
A RIGHT JOIN B           A RIGHT OUTER JOIN B
A FULL JOIN B            A FULL OUTER JOIN B
A INNER JOIN B           A JOIN B


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

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

جدول Bookings به همراه دو کلید خارجی به جداول Facilities و Members است:
namespace EFCorePgExercises.Entities
{
    public class Booking
    {
       // ...

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

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

       // ...
    }
}
بنابراین برای تولید گزارشی که اطلاعات هر دوی این‌ها را به همراه دارد (اطلاعات کاربر و اطلاعات امکاناتی که استفاده کرده)، نیاز است دو جوین به دو جدول یاد شده نوشته شود. برای اینکار نیاز است در کوئری خود به booking.Member و booking.Facility برسیم. به همین جهت از جدول کاربران که دارای خاصیت از نوع ICollection  اشاره کننده‌ی به Bookings کاربران است شروع می‌کنیم:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
سپس بر روی این خاصیت مجموعه‌ای، اینبار یک SelectMany را فراخوانی می‌کنیم تا خروجی آن، تک تک رکوردهای booking متناظر باشد. اکنون که به هر رکورد booking کاربران دسترسی یافته‌ایم، می‌توانیم از طریق خواص راهبری booking.Member و booking.Facility هر ردیف، اطلاعات نهایی گزارش را تولید کنیم:
int[] tennisCourts = { 0, 1 };
var members = context.Members
                        .SelectMany(x => x.Bookings)
                        .Where(booking => tennisCourts.Contains(booking.Facility.FacId))
                        .Select(booking => new
                        {
                            Member = booking.Member.FirstName + " " + booking.Member.Surname,
                            Facility = booking.Facility.Name
                        })
                        .Distinct()
                        .OrderBy(x => x.Member)
                        .ToList();
ID زمین‌های تنیس مشخص هستند که توسط tennisCourts.Contains به FacId‌های موجود اعمال شده‌اند. همچنین در قسمت Select نیز خاصیت Member آن به جمع دو خاصیت از booking.Member اشاره می‌کند و چون نتیجه‌ی حاصل یک ستون از پیش تعریف شده نیست، نیاز است تا برای آن نام صریحی انتخاب شود.
پس از آن برای حذف ردیف‌های تکراری حاصل از گزارش، از متد Distinct استفاده شده و OrderBy نیز بر اساس خاصیت جدید Member، قابل تعریف است:



مثال 6: تولید لیست رزروهای گران قیمت

لیست رزروهای روز 2012-09-14 را تولید کنید که هزینه‌ی آن‌ها بیشتر از 30 دلار باشد. باید بخاطر داشت که هزینه‌های کاربران با مهمان‌ها متفاوت است و هزینه‌ها بر اساس Slotهای نیم ساعته محاسبه می‌شوند و ID کاربر مهمان همیشه صفر است. خروجی  این گزارش باید به همراه نام کامل کاربر، نام امکانات مورد استفاده و هزینه‌ی نهایی باشد. همچنین باید بر اساس هزینه‌های نهایی به صورت نزولی مرتب شود.
var date1 = new DateTime(2012, 09, 14);
var date2 = new DateTime(2012, 09, 15);

var items = context.Members
                        .SelectMany(x => x.Bookings)
                        .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2
                        && (
                            (((booking.Slots * booking.Facility.GuestCost) > 30) && (booking.MemId == 0)) ||
                            (((booking.Slots * booking.Facility.MemberCost) > 30) && (booking.MemId != 0))
                        ))
                        .Select(booking => new
                        {
                            Member = booking.Member.FirstName + " " + booking.Member.Surname,
                            Facility = booking.Facility.Name,
                            Cost = booking.MemId == 0 ?
                                        booking.Slots * booking.Facility.GuestCost
                                        : booking.Slots * booking.Facility.MemberCost
                        })
                        .Distinct()
                        .OrderByDescending(x => x.Cost)
                        .ToList();
در اینجا نیز چون نیاز است خروجی نهایی به همراه نام کاربر و نام امکانات مورد استفاده باشد، همانند مثال قبلی، به حداقل دو جوین نیاز است. به همین جهت از جدول Members به همراه SelectMany بر روی تک تک Bookings آن شروع می‌کنیم.
سپس بر اساس صفر بودن یا نبودن booking.MemId  (کاربر مهمان بودن یا خیر)، شرط هزینه‌ی بیشتر از 30 دلار اعمال شده‌است.
در آخر Select گزارش مورد نیاز، به همراه جمع نام و نام خانوادگی، نام امکانات استفاده شده و خاصیت محاسباتی Cost است که بر اساس مهمان بودن یا نبودن کاربر، متفاوت است.
متد Distinct ردیف‌های تکراری حاصل از این گزارش را حذف می‌کند (محل درج آن مهم است) و متد OrderByDescending، مرتب سازی نزولی بر اساس خاصیت محاسباتی Cost را انجام می‌دهد.



مثال 7: تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها، بدون استفاده از جوین.

در اینجا می‌خواهیم همان مثال 4 را بدون استفاده از جوین بررسی کنیم. بدون استفاده از جوین در اینجا به معنای استفاده از sub-query است (نوشتن یک کوئری داخل کوئری اصلی).
var members = context.Members
                        .Select(member =>
                        new
                        {
                            Member = member.FirstName + " " + member.Surname,
                            Recommender = context.Members
                                .Where(recommender => recommender.MemId == member.RecommendedBy)
                                .Select(recommender => recommender.FirstName + " " + recommender.Surname)
                                .FirstOrDefault() ?? ""
                        })
                        .Distinct()
                        .OrderBy(member => member.Member)
                        .ToList();
این کوئری به صورت متداولی بر روی جدول Members اعمال شده‌است، با این تفاوت که در حین Select نهایی آن، یکبار دیگر کوئری جدید شروع شده‌ی با context.Members را مشاهده می‌کنید که سبب تولید یک sub-query، زمانیکه ToList نهایی فراخوانی می‌شود، خواهد شد. این sub-query در حقیقت یک outer join را با ذکر recommender.MemId == member.RecommendedBy (بیان صریح روش اتصال ID‌های دو سر رابطه) شبیه سازی می‌کند.



مثال 8: تولید لیست رزروهای گران قیمت با استفاده از یک sub-query.

هدف از این مثال، ارائه‌ی روش حل دیگری برای مثال 6، به نحو تمیزتری است. در مثال 6، هزینه‌ی رزرو را دوبار، یکبار در متد Where و یکبار در متد Select محاسبه کردیم. اینبار می‌خواهیم با استفاده از sub-query‌ها این محاسبه را یکبار انجام دهیم.
var date1 = new DateTime(2012, 09, 14);
var date2 = new DateTime(2012, 09, 15);

var items = context.Members
                        .SelectMany(x => x.Bookings)
                        .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2)
                        .Select(booking => new
                        {
                            Member = booking.Member.FirstName + " " + booking.Member.Surname,
                            Facility = booking.Facility.Name,
                            Cost = booking.MemId == 0 ?
                                        booking.Slots * booking.Facility.GuestCost
                                        : booking.Slots * booking.Facility.MemberCost
                        })
                        .Where(x => x.Cost > 30)
                        .Distinct()
                        .OrderByDescending(x => x.Cost)
                        .ToList();
اینبار یک Select نوشته شده که در آن Cost، در ابتدا محاسبه شده و سپس Where دومی ذکر شده که از این Cost استفاده می‌کند.
هرچند کوئری SQL نهایی تولید شده‌ی توسط EF-Core آن، تفاوتی چندانی با نگارش قبلی ندارد:



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
اشتراک‌ها
ویژگی های جدید dotNET Core 2.1

Earlier this week, Microsoft published the roadmap for .NET Core 2.1, ASP.NET Core 2.1 and EF Core 2.1, expected to be out in the first quarter of 2018. The team also talked about several new features with this new release. This release is more of a feedback-oriented release based on .NET Core 2.0 release. The.NET Core 2.0 is a huge success and already more than half a million developers are now using .NET Core 2.0. All thanks to .NET Standard 2.0 release . In this post find out about the new features of .NET Core 2.1. 

ویژگی های جدید dotNET Core 2.1