نظرات مطالب
پایان پروژه ASP.NET Ajax Control Toolkit !
ضمنا یک مورد رو در باره‌ی LINQ to SQL و ASP کلاسیک اضافه کنم. جایگزین شدن entity framework بجای L2S یا ASP.NET به جای ASP کلاسیک، یک روند سالم و سلامت توسعه است. LINQ to SQL فقط محدود است به SQL Server اما الان اکثر بانک‌های اطلاعاتی موجود پروایدر EF دارند و مدل توسعه‌ی آن بسته نیست. ASP کلاسیک رو نمی‌دونم باهاش کار کرده بودید یا نه؟ رسما یک فاجعه بود! مخلوطی از کدهای برنامه داخل کدهای HTML و وابستگی آن به اشیاء COM و غیره (اگر می‌خواستید مثلا رمزنگاری را به آن اضافه کنید باید Active-X می‌نوشتید و در سرور رجیستر می‌کردید!). این مورد اصلا قابل قیاس نیست با ASP.NET و امکانات دات نت فریم ورک.
مطالب
بررسی ORM های مناسب جهت استفاده در اندروید
با آمدن ORM‌ها به دنیای برنامه نویسی، کار برنامه نویسی نسبت به قبل ساده‌تر و راحت‌تر شد. عدم استفاده کوئری‌های دستی، پشتیبانی از چند دیتابیس و از همه مهمتر و اصلی‌ترین هدف این ابزار "تنها درگیری با اشیا و مدل شیء گرایی" کار را پیش از پیش آسان‌تر نمود.
در این بین به راحتی می‌توان چندین نمونه از این ORM‌ها را  نام برد مثل IBatis , Hibernate ,Nhibernate و EF که از معروفترین آن‌ها هستند.
من در حال حاضر قصد شروع یک پروژه اندرویدی را دارم و دوست دارم بجای استفاده‌ی از Sqlitehelper، از یک ORM مناسب بهره ببرم که چند سوال برای من پیش می‌آید. آیا ORM ای برای آن تهیه شده است؟ اگر آری چندتا و کدامیک از آن‌ها بهتر هستند؟ شاید در اولین مورد کتابخانه‌ی Hibernate جاوا را نام ببرید؛ ولی توجه به این نکته ضروری است که ما در مورد پلتفرم موبایل و محدودیت‌های آن صحبت می‌کنیم. یک کتابخانه همانند Hibernate مطمئنا برای یک برنامه اندروید چه از نظر حجم نهایی برنامه و چه از نظر حجم بزرگش در اجرا، مشکل زا خواهد بود و وجود وابستگی‌های متعدد و دارا بودن بسیاری از قابلیت‌هایی که اصلا در بانک‌های اطلاعاتی موبایل قابل اجرا نیست، باعث می‌شود این فریمورک انتخاب خوبی برای یک برنامه اندروید نباشد.

معیارهای انتخاب یک فریم ورک مناسب برای موبایل:
  • سبک بودن: مهمترین مورد سبک بودن آن است؛ چه از لحاظ اجرای برنامه و چه از لحاظ حجم نهایی برنامه
  • سریع بودن: مطمئنا ORM‌های طراحی شده‌ی موجود، از سرعت خیلی بدی برخوردار نخواهند بود؛ اگر سر زبان هم افتاده باشند. ولی باز هم انتخاب سریع بودن یک ORM، مورد علاقه‌ی بسیاری از ماهاست.
  • یادگیری آسان و کانفیگ راحت تر.

OrmLight

این فریمورک مختص اندروید طراحی نشده ولی سبک بودن آن موجب شده‌است که بسیاری از برنامه نویسان از آن در برنامه‌های اندرویدی استفاده کنند. این فریم ورک جهت اتصالات JDBC و Spring و اندروید طراحی شده است.

نحوه معرفی جداول در این فریمورک به صورت زیر است:
@DatabaseTable(tableName = "users")
public class User {
    @DatabaseField(id = true)
    private String username;
    @DatabaseField
    private String password;
 
    public User() {
        // ORMLite needs a no-arg constructor
    }
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
 
    // Implementing getter and setter methods
    public String getUserame() {
        return this.username;
    }
    public void setName(String username) {
        this.username = username;
    }
    public String getPassword() {
        return this.password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
با استفاده از کلمات کلیدی DatabaseTable@ در بالای کلاس و DatabaseField@ در بالای هر پراپرتی به معرفی جدول و فیلدهای جدول می‌پردازیم.
سورس این فریمورک را می‌توان در گیت هاب یافت و مستندات آن در این آدرس قرار دارند.


SugarORM
این فریمورک مختص اندروید طراحی شده است. یادگیری آن بسیار آسان است و به راحتی به یاد می‌ماند. همچنین جداول مورد نیاز را به طور خودکار خواهد ساخت. روابط یک به یک و یک به چند را پشتیبانی می‌کند و عملیات CURD را با سه متد Save,Delete و Find که البته FindById هم جزء آن است، پیاده سازی می‌کند.

برای استفاده از این فریمورک نیاز است ابتدا متادیتا‌های زیر را به فایل manifest اضافه کنید:
<meta-data android:name="DATABASE" android:value="my_database.db" />
<meta-data android:name="VERSION" android:value="1" />
<meta-data android:name="QUERY_LOG" android:value="true" />
<meta-data android:name="DOMAIN_PACKAGE_NAME" android:value="com.my-domain" />
برای تبدیل یک کلاس به جدول هم از کلاس این فریم ورک ارث بری می‌کنیم:
public class User extends SugarRecord<User> {
    String username;
    String password;
    int age;
    @Ignore
    String bio; //this will be ignored by SugarORM
 
    public User() { }
 
    public User(String username, String password,int age){
        this.username = username;
        this.password = password;
        this.age = age;
    }
}
بر خلاف OrmLight که باید فیلد جدول را معرفی می‌کردید، اینجا تمام پراپرتی‌ها به اسم فیلد شناخته می‌شوند؛ مگر اینکه در بالای آن از عبارت Ignore@ استفاده کنید.

باقی عملیات آن از قبیل اضافه کردن یک رکورد جدید یا حذف رکورد(ها) به صورت زیر است:
User johndoe = new User(getContext(),"john.doe","secret",19);
johndoe.save(); //ذخیره کاربر جدید در دیتابیس


//حذف تمامی کاربرانی که سنشان 19 سال است
List<User> nineteens = User.find(User.class,"age = ?",new int[]{19});
foreach(user in nineteens) {
    user.delete();
}
برای اطلاعات بیشتر به مستندات آن رجوع کنید.


GreenDAO
موقعیکه بحث کارآیی و سرعت پیش می‌آید نام GreenDAO هست که می‌درخشد. طبق گفته‌ی سایت رسمی آن این فریمورک میتواند در ثانیه چند هزار موجودیت را اضافه و به روزرسانی و بارگیری نماید. این لیست حاوی برنامه‌هایی است که از این فریمورک استفاده می‌کنند. جدول زیر مقایسه‌ای است بین این کتابخانه و OrmLight که نشان میدهد 4.5 برابر سریعتر از OrmLight عمل می‌کند.

غیر از این‌ها در زمینه‌ی حجم هم حرف‌هایی برای گفتن دارد. حجم این کتابخانه کمتر از 100 کیلوبایت است که در اندازه‌ی APK اثر چندانی نخواهد داشت.

آموزش راه اندازی  آن در اندروید استادیو، سورس آن در گیت هاب و مستندات رسمی آن.


Active Android
این کتابخانه از دو طریق فایل JAR و به شیوه maven قابل استفاده است که می‌توانید روش استفاده‌ی از آن را در این لینک ببینید و سورس اصلی آن هم در این آدرس قرار دارد. بعد از اینکه کتابخانه را به پروژه اضافه کردید، دو متادیتای زیر را که به ترتیب نام دیتابیس و ورژن آن هستند، به manifest اضافه کنید:
<meta-data android:name="AA_DB_NAME" android:value="my_database.db" />
<meta-data android:name="AA_DB_VERSION" android:value="1" />
بعد از آن  عبارت ;()ActiveAndroid.Initialize را در اکتیویتی‌های مدنظر اعمال کنید:
public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActiveAndroid.initialize(this);
 
        //ادامه برنامه
    }
}
برای معرفی کلاس‌ها به جدول هم از دو اعلان Table و Column مانند کد زیر به ترتیب برای معرفی جدول و فیلد استفاده می‌کنیم.
@Table(name = "User")
public class User extends Model {
    @Column(name = "username")
    public String username;
 
    @Column(name = "password")
    public String password;
 
    public User() {
        super();
    }
 
    public User(String username,String password) {
        super();
        this.username = username;
        this.password = password;
    }
}
جهت اطلاعات بیشتر در مورد این کتابخانه به مستندات آن رجوع کنید.


ORMDroid
 از آن دست کتابخانه‌هایی است که سادگی و کم حجم بودن شعار آنان است و سعی دارند تا حد ممکن همه چیز را خودکار کرده و کمترین کانفیگ را نیاز داشته باشد. حجم فعلی آن حدود 20 کیلوبایت بوده و نمی‌خواهند از 30 کیلوبایت تجاوز کند.

برای استفاده‌ی از آن ابتدا دو خط زیر را جهت معرفی تنظیمات به manifest اضافه کنید:
<meta-data
  android:name="ormdroid.database.name"
  android:value="your_database_name" />

<meta-data
  android:name="ormdroid.database.visibility"
  android:value="PRIVATE||WORLD_READABLE||WORLD_WRITEABLE" />
برای آغاز کار این کتابخانه، عبارت زیر را در هرجایی که مایل هستید مانند کلاس ارث بری شده از Application یا در ابتدای هر اکتیویتی که مایل هستید بنویسید. طبق مستندات آن صدا زدن چندباره این متد هیچ اشکالی ندارد.
ORMDroidApplication.initialize(someContext);
معرفی مدل جدول بانک اطلاعاتی هم از طریق ارث بری از کلاس Entity می‌باشد .
public class Person extends Entity {
  public int id;
  public String name;
  public String telephone;
}

//====================

Person p = Entity.query(Person.class).where("id=1").execute();
p.telephone = "555-1234";
p.save();

// یا

Person person = Entity.query(Person.class).where(eql("id", id)).execute();
p.telephone = "555-1234";
p.save();
کد بالا دقیقا یادآوری به EF هست ولی حیف که از Linq پشتیبانی نمی‌شود.
سورس آن در گیت هاب

در اینجا سعی کردیم تعدادی از کتابخانه‌های محبوب را معرفی کنیم ولی تعداد آن به همین جا ختم نمی‌شود. ORM‌های دیگری نظیر AndRom و سایر ORM هایی که در این لیست معرفی شده اند وجود دارند.


نکته نهایی اینکه خوب می‌شود دوستانی که از این ORM‌های مختص اندروید استفاده کرده اند؛ نظراتشان را در مورد آن‌ها بیان کنند و مزایا و معایب آن‌ها را بیان کنند.
مطالب
بررسی وجود نام کاربر با استفاده از jQuery Ajax در ASP.Net

شاید بعضی از سایت‌ها را دیده باشید که در حین ثبت نام، پس از وارد کردن یک نام کاربری و سپس مشغول شدن به پر کردن فیلد کلمه‌ی عبور، در قسمت نام کاربری شروع به جستجو در مورد آزاد بودن نام کاربری درخواستی می‌کنند یا نمونه‌ای دیگر، فرم پرداخت الکترونیکی بانک سامان. پس از اینکه شماره قبض را وارد کردید، بلافاصله بدون ریفرش صفحه به شما پیغام می‌دهد که این شماره معتبر است یا خیر. امروز قصد داریم این قابلیت را با استفاده از کتابخانه‌ی Ajax مجموعه jQuery در ASP.Net پیاده سازی کنیم (بدون استفاده از ASP.Net Ajax مایکروسافت).
ابتدا سورس کامل را ملاحظه نمائید:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AjaxTest.aspx.cs" Inherits="testWebForms87.AjaxTest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>jQuery Ajax Text</title>

<script src="jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#<%= TextBox1.ClientID %>").blur(function(event) {
$.ajax({
type: "POST",
url: "AjaxTest.aspx/IsUserAvailable",
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
$('#valid').html("<img src='ajaxImages/waiting.gif' alt='لطفا کمی تامل کنید'>");
var delay = function() {
AjaxSucceeded(msg);
};

setTimeout(delay, 2000); //remove this
},
error: AjaxFailed
});
});
});
function AjaxSucceeded(result) {
if (result.d == true)
$('#msg').html("<img src='ajaxImages/available.gif' alt='نام کاربری درخواستی موجود است'>");
else
$('#msg').html("<img src='ajaxImages/taken.gif' alt='متاسفانه نام کاربری مورد نظر پیشتر دریافت شده‌است'>");
}
function AjaxFailed(result) {
alert(result.status + ' ' + result.statusText);
}
</script>

</head>
<body>
<form id="form1" runat="server">
<div>
user name:
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<span id="msg"></span>
<br />
pass:
<asp:TextBox ID="TextBox2" TextMode="Password" runat="server"></asp:TextBox>
</div>
<!-- preload -->
<div style="display: none">
<img src="ajaxImages/available.gif" alt="available" />
<img src="ajaxImages/taken.gif" alt="taken" />
<img src="ajaxImages/waiting.gif" alt="waiting" />
</div>
</form>
</body>
</html>


using System;
using System.Web.Services;

namespace testWebForms87
{
public partial class AjaxTest : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}

[WebMethod]
public static bool IsUserAvailable(string username)
{
// این مورد را با خواندن اطلاعات از دیتابیس می‌شود تعویض کرد
return username != "test";
}
}
}

توضیحات:
همانطور که ملاحظه می‌کنید صفحه‌ی ASP.Net ما بسیار ساده است و از دو تکست باکس استاندارد تشکیل می‌شود، به همراه تصاویر مربوط به Ajax که یک سری تصاویر ساده چرخان معروف منتظر بمانید ، یافت شد یا موجود نیست می‌باشند. این تصاویر در یک div مخفی (display: none) در صفحه قرار گرفته‌اند و در هنگام بارگذاری صفحه، این‌ها نیز بارگذاری شده و حاضر و آماده خواهند بود. بنابراین هنگام استفاده از آن‌ها، کاربر تاخیری را مشاهده نخواهد کرد. همچنین یک span با id مساوی msg‌ را هم پس از تکست باکس اضافه کرده‌ایم تا تصاویر مربوط به رخدادهای Ajax را با استفاده از توانایی‌های jQuery به آن اضافه کنیم.

اسکریپت Ajax ما با دراختیار گرفتن روال رخداد گردان blur شیء textBox1 شروع می‌شود. همانطور که در مقالات پیشین سایت نیز ذکر شد، روش صحیح دریافت ID یک کنترل ASP.Net در کدهای سمت کلاینت جاوا اسکریپتی، بر اساس خاصیت ClientID آن است که در اولین سطر کدهای ما مشخص است (زیرا در ASP.Net نام و ID یک کنترل در هنگام رندر شدن به همراه ID کنترل‌های دربرگیرنده آن نیز خواهد بود، بنابراین بهتر است این مورد را داینامیک کرد).

کار بررسی موجود بودن نام کاربری (یا مثلا یک شماره قبض و امثال آن) توسط WebMethod ایی به نام IsUserAvailable در code behind صفحه انجام می‌شود که پیاده سازی آن‌را ملاحظه می‌کنید. بدیهی است در این مثال ساده، تنها نام کاربری از پیش رزرو شده، کلمه‌ی test است و در یک کد واقعی این مورد با مقایسه‌ی نام کاربری با اطلاعات موجود در دیتابیس باید صورت گیرد (و حملات تزریق اس کیوال را هم فراموش نکنید. برای رهایی از آن‌ها "حتما" باید از پارامترهای ADO.Net استفاده کرد و گرنه کد شما مستعد به این نوع حملات خواهد بود).

سؤال: چرا از web method استفاده شد و همچنین چرا این متد static است؟
زمانیکه یک متد با کلمه کلیدی static‌ مشخص می‌شود حالت state less پیدا می‌کند یعنی مستقل از وهله‌ی کلاس عمل می‌کند. در این حالت نیازی به ارسال ViewState نبوده (بنابراین در کوئری مورد نظر ما بسیار بهینه و سبک عمل می‌کنند) و همچنین نیازی به ایجاد یک وهله‌ای از کلاس صفحه‌ی ما نیز نخواهد بود. برای توضیحات بیشتر به این مقاله مراجعه نمائید. (به صورت خلاصه، دلیل اصلی، کارآیی بالا و بهینه بودن این روش در این مساله ویژه است و در ASP.Net Ajax مایکروسافت به صورت گسترده‌ای در پشت صحنه مورد استفاده قرار می‌گیرد)
استفاده از ویژگی WebMethod عملکرد صفحه‌ی ما را شبیه به یک وب سرویس خواهد کرد و امکان دسترسی به آن در متدهای استاندارد POST به صورت ارسال دیتا به آدرس WebService.asmx/WebMethodName‌ خواهد بود. یک مثال ساده و عملی

بررسی تابع Ajax بکار رفته:
این تابع هنگام فراخوانی رخداد blur تکست‌باکس ما (مطابق کد فوق) فراخوانی می‌شود. ساختار ساده‌ای دارد که به شرح زیر است:
type: "POST"
نحوه‌ی ارسال داده را به متد وب سرویس ما مشخص می‌کند.

url: "AjaxTest.aspx/IsUserAvailable"
اطلاعات پست شده، به صفحه‌ی AjaxTest.aspx و وب متد IsUserAvailable ارسال خواهد شد

data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
داده‌ای که به آرگومان username ما ارسال می‌شود، همان مقدار تایپ شده در TextBox1 است (که باز هم دریافت ID آن به صورت داینامیک صورت گرفته تا مشکل زا نشود)

contentType: "application/json; charset=utf-8",
dataType: "json",
در این دو سطر از نوع داده‌ی json استفاده شده است که فرمت بسیار سبک و بهینه‌ای برای تبادل اطلاعات در وب به‌شمار می‌آید و توسط کتابخانه‌های جاوا اسکریپتی به سادگی پردازش شده و تبدیل به اشیاء مورد نظر خواهند شد. برای مثال اگر خروجی یک وب سرویس در حالت xml به صورت زیر باشد:

<xx yy="nn"></xx>
معادل json آن به شرح زیر است:
{ "xx": {"yy":"nn"} }
success: function(msg)
Success ، پس از موفقیت آمیز بودن عملیات ajax در jQuery فراخوانی می‌شود
error: AjaxFailed
و اگر در این بین خطایی رخ داده باشد، قسمت error فراخوانی می‌شود.

در این مثال برای نمایش بهتر عملیات، یک وقفه‌ی 2 ثانیه‌ای توسط setTimeout ایجاد شده و بدیهی است در یک مثال واقعی باید آن‌را حذف نمود.

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





نظرات مطالب
استفاده از افزونه‌ی jQuery Autocomplete در ASP.NET
در مثال جاری با توجه به اینکه ورودی دریافتی از کاربر به کمک SQL پارامتری توسط ADO.NET پردازش می‌شود، مقاومت در برابر تزریق اس‌کیوال آن افزایش یافته است.
نظرات اشتراک‌ها
راهنمای مایکروسافت در مورد مقابله با SQL Injection
نیاز به code review دارید و از راه دور نمیشه پاسخ دقیق به شما داد.
+ الزامی نداره که حتما مشکل تزریق اس کیوال سبب بروز این مساله شده. ممکن است تونسته باشند به web.config شما دسترسی پیدا کنند.
+ روش کاملتر برای مقابله با ورودی‌های خطرناک: در اینجا
نظرات اشتراک‌ها
دوراهی انتخاب NHibernate و Entityframework

شما هنوز از ie 8 استفاده می‌کنید؟!

نویسندگان این سایت که قرار نیست برای همه بیان بگن مثلا می‌تونی کوئری‌های لینک رو با تاخیر اجرا کنی. مثلا deferred execution هم وجود داره برای ترکیب چند کوئری با هم. یا این کوئری‌ها همشون پارامتری هستند و مثل رویه‌های ذخیره شده کش میشن. 

مطالب
حذف جوین‌های اضافی به یک جدول در 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
مطالب
شرط گذاری روی Include ها در EF Core 5x
سناریویی را در نظر بگیرید که میخواهید لیست Blog‌ها را به همراه Post هایشان که شامل کلمه‌ی خاصی است، به کلاینت باز گردانید. در این حالت احتمالا چنین کدی به نظرتان خواهد آمد:
// -- FilteredInclude_EFCore5
var list = dbContext.Blogs
    .AsNoTracking()
    .Include(p => p.Posts.Where(p => p.Title.Contains("test title")))
    .ToList();
return Json(list);
این کد تا قبل از EFCore 5.0 پیش نمایش 3، به خطای زیر منجر میشود؛ چرا که EFCore از شرط گذاری روی Include‌ها پشتیبانی نمی‌کند:
System.InvalidOperationException: 'Lambda expression used inside Include is not valid.'
پس مجبوریم همه‌ی رکورد‌های Include را از دیتابیس خوانده و سپس آنها را در حافظه فیلتر کنیم:
// -- NonFilteredInclude
var list = dbContext.Blogs
    .AsNoTracking()
    .Include(e => e.Posts)
    .ToList();
list.ForEach(p => p.Posts = p.Posts.Where(p => p.Title.Contains("test title")).ToList());
این روش سربار بسیار زیادی دارد و بسته به تعداد رکورد‌ها و ستون‌های Post، حجم زیادی از دیتای غیر لازم را از دیتابیس میخواند و تخصیص حافظه (memory allocation) اضافی و زیادی را به همراه دارد. مثلا اگر 100 Blog داشته باشیم که هرکدام 100 Post داشته باشند و فقط یکی از Post‌ها شرط مورد نظر را داشته باشد، بدین ترتیب 100 * 100 منهای 1 رکورد اضافی واکشی خواهد شد؛ یعنی برابر ‭9,999‬! (می توان با لحاظ کردن تعداد و حجم ستون‌های اضافی نیز وخامت اوضاع را درک کرد)
همچنین اگر به صورت غیر read-only (عدم استفاده از AsNoTracking)  داده‌ها را لود کرده باشید، با شرطی که داخل ForEach اعمال می‌شود، رکوردهایی که فیلتر میشوند به صورت Deleted در ChangeTracker علامت گذاری میشوند که میتواند مشکل ساز نیز باشد.
برای حل این مشکل چندین روش وجود دارد:
1- توسط یک تایپ دلخواه (anonymous یا dto) واکشی را به صورت Projection انجام دهیم و Post‌ها را فیلتر کنیم:
// -- Projection_Manually
var list = dbContext.Blogs
    .AsNoTracking()
    .Select(p => new
    {
        p.Id,
        p.Name,
        Posts = p.Posts.Where(p => p.Title.Contains("test title")).ToList()
    }).ToList();
این دستور، کوئری SQL زیر را تولید میکند:
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Description], [t].[Title]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title]
    FROM [Posts] AS [p]
    WHERE CHARINDEX(N'test title', [p].[Title]) > 0
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Id]
معایب این روش:
  • در صورت نیاز به ویرایش (عدم استفاده از AsNoTracking) بدلیل استفاده از anonymous بجای Blog، هیچ شیء Blog ایی در ChangeTracker ثبت نخواهد شد، ولی اشیا Post در ChangeTracker ثبت می‌شوند. در نتیجه تنها 1 شیء در ChangeTracker اضافه خواهد شد.
  • کد نویسی را کثیف میکند؛ مخصوصا اگر نیاز به شرط گذاری بر روی چندین Navigation Collection تو در تو را داشته باشید.
برای جلوگیری از این کثیف شدن میتوان از قابلیت Projection کتابخانه‌ی AutoMapper استفاده کرد. کوئری تولید شده و عملکرد آن عینا مشابه همین روش است، ولی کد تمیز‌تری را موجب می‌شود ( از نظر سرعت، مقدار کمی کند‌تر است. انتهای مقاله، بنچمارک آن را میتوانید مشاهده کنید)

2- از قابلیت IncludeFilter کتابخانه‌ی  Z.EntityFramework.Plus.EFCore استفاده کنیم.
این کتابخانه امکانات بسیار مفیدی را ارائه میدهد و شخصا برای پروژه‌های واقعی و بزرگ آن را پیشنهاد میدهم. اگر از امکانات آن بجا استفاده شود، تاثیر بسیار زیادی را بر روی پرفرمنس پروژه خواهد گذاشت (توصیه میکنم حتما داکیومنت آن را مطالعه کنید). این کتابخانه کاملا رایگان است و از EFCore و EF6 (در یک پکیج جداگانه) پشتیبانی میکند. شرکت مالک آن (ZZZ) یک کتابخانه‌ی دیگر را نیز به نام  Z.EntityFramework.Extensions.EFCore دارد که امکانات بیشتری را ارائه میدهد؛ ولی رایگان نیست.
در این روش خواهیم داشت:
// -- IncludeFilter_EFCorePlus
var list = dbContext.Blogs
    .AsNoTracking()
    .IncludeFilter(e => e.Posts.Where(p => p.Title.Contains("test tile")))
    .ToList();
این دستور کوئری SQL زیر را تولید میکند:
-- EF+ Query Future: 1 of 2
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
;

-- EF+ Query Future: 2 of 2
SELECT [t].[Id], [t].[BlogId], [t].[Description], [t].[Title]
FROM [Blogs] AS [b]
INNER JOIN (
    SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title]
    FROM [Posts] AS [p]
    WHERE CHARINDEX(N'test title', [p].[Title]) > 0
) AS [t] ON [b].[Id] = [t].[BlogId]
;
  • همانطور که می‌بینید این دستور، 2 کوئری را اجرا میکند. سرعت آن از روش قبلی کمی کند‌تر است و memory allocation بیشتری را انجام میدهد.
  • در صورت عدم استفاده از AsNoTracking، اشیاء Blog را نیز ثبت میکند؛ درنتیجه تعداد 101 آبجکت (100 Blog و 1 Post) به ChangeTracker اضافه خواهند شد.
  • کد نویسی تمیزتر و راحت‌تری در سمت سی شارپ دارد. 
  • این روش در EF6 نیز قابل استفاده است.

3- کمبود این قابلیت در EFCore بسیار حس میشد (در NHibernate از قدیم این امکان وجود داشت) تا اینکه نهایتا در EFCore 5.0 پیش نمایش 3 (آخرین نسخه‌ی در حال حاضر) این قابلیت به EFCore اضافه شده‌است.
برای استفاده از آن نیاز به هیچ کد اضافه‌ای نیست و به صورت معمول میتوان از متد Include، همراه با شرط استفاده کرد:
// -- FilteredInclude_EFCore5
var list = dbContext.Blogs
    .AsNoTracking()
    .Include(p => p.Posts.Where(p => p.Title.Contains("test title")))
    .ToList();
این دستور، کوئری SQL زیر را تولید میکند:
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Description], [t].[Title]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title]
    FROM [Posts] AS [p]
    WHERE CHARINDEX(N'test title', [p].[Title]) > 0
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Id]
  • این روش بسیار بهینه است و از روش قبلی (دوم) کمی سریع‌تر بوده و memory allocation کمتری (نزدیک به روش اول) دارد.
  • در صورت عدم استفاده از AsNoTracking، مانند قبلی عمل میکند؛ درنتیجه تعداد 101 آبجکت به ChangeTracker اضافه خواهند شد. 
  • کد نویسی تمیزتر و راحت‌تری در سمت سی شارپ دارد.

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

مطالب
استفاده از Dialect سفارشی در NHibernate

Dialects در NHibernate کلاس‌هایی هستند جهت معرفی تعاریف ویژگی‌های خاص بانک‌های اطلاعاتی مختلف؛ مثلا SQL Server 2008 چه ویژگی‌های جدیدی دارد یا SQL Server CE 4.0 که جدیدا ارائه شده، امکان تعریف offset را در کوئری‌های خود میسر کرده (چیزی که قرار است در نگارش بعدی SQL Server اصلی(!) در دسترس باشد) ، اکنون چگونه می‌توان این ویژگی را فعال کرد (باید Dialect آن به روز شود و ... همین). یک سری Dialect از پیش تعریف شده هم برای اکثر بانک‌های اطلاعاتی در NHibernate وجود دارد. ممکن است این Dialects پیش فرض الزاما خواسته شما را برآورده نکنند یا مو به مو مستندات بانک‌ اطلاعاتی مرتبط را پیاده سازی نکرده باشند و سؤال این است که اکنون چه باید کرد؟ آیا باید حتما سورس‌ها را دستکاری و بعد کامپایل کرد؟ به این صورت هر بار با ارائه یک نگارش جدید NHibernate به مشکل برخواهیم خورد چون باید کل عملیات تکرار شود.
خبر خوب اینکه می‌توان از همین Dialects موجود ارث بری کرد، سپس مواردی را که نیاز داریم override کرده یا به کلاس مشتق شده افزود. اکنون می‌توان از این Dialect سفارشی به جای Dialect اصلی استفاده کرد. در ادامه با یک نمونه آشنا خواهیم شد.
فرض کنید Dialect انتخابی مرتبط است با SQL Server CE استاندارد. کوئری ساده زیر را می‌نویسیم، به ظاهر باید کار کند:
var list = session.Query<SomeClass>().Where(x=>x.Date.Year==2011).ToList();
اما کار نمی‌کند! علت این است که تمام Dialects در NHibernate از یک Dialect پایه مشتق شده‌اند. در این Dialect پایه، تعریف تابع استخراج year از یک تاریخ به نحو زیر است:
extract(year, ?1)
اما در SQL CE این تابع باید به صورت زیر تغییر کند تا کار کند:
datepart(year, ?1)
و ... این Override انجام نشده (تا نگارش فعلی آن). مهم نیست! خودمان انجام خواهیم داد! به صورت زیر:
using NHibernate;
using NHibernate.Dialect;
using NHibernate.Dialect.Function;

namespace Test1
{
public class CustomSqlCeDialect : MsSqlCeDialect
{
public CustomSqlCeDialect()
{
RegisterFunction("year", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(year, ?1)"));
}
}
}
خوب تا اینجا ما یک Dialect جدید را با ارث بری از MsSqlCeDialect اصلی تولید کرده‌ایم. مرحله بعد نوبت به معرفی آن به NHibernate است. اینکار توسط Fluent NHibernate به سادگی زیر است:
var dbType = MsSqlCeConfiguration.Standard
...
.Dialect<CustomSqlCeDialect>();

پس از آن کوئری LINQ ابتدای بحث بدون مشکل اجرا خواهد شد چون اکنون می‌داند که بجای extract year ، باید از تابع datepart‌ استفاده کند.
مرحله بعد هم می‌تواند تهیه یک patch و ارسال به گروه اصلی برای به روز رسانی پروژه NH باشد.

مطالب
یافتن مقادیر نال در Entity framework
کلاس شخص زیر را درنظر بگیرید
public class Person
{
        public int Id { get; set; }                
        public string Name { get; set; }
        public int? Age { get; set; }
}
در اینجا با توجه به اینکه Name از نوع string است، خودبخود به فیلدی نال‌پذیر نگاشت خواهد شد و همچنین Age عددی نیز در سمت کدهای ما Nullable است، بنابراین خاصیت سن هم به فیلدی نال‌پذیر نگاشت می‌شود.
اگر تمام مراحل متداول ایجاد Context را طی کنیم، به نظر شما خروجی SQL عبارت زیر چه خواهد بود؟
string name = null;
var list1 = ctx.Users.Where(x => x.Name == name).ToList();
در این عبارت، name به صورت یک متغیر ارسال شده است و نه یک مقدار ثابت (فرض کنید یک متد را تعریف کرده‌اید که name را به صورت پارامتر دریافت می‌کند).
خروجی SQL آن به نحو زیر است:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Age] AS [Age]
FROM [dbo].[People] AS [Extent1]
WHERE [Extent1].[Name] = @p__linq__0
-- p__linq__0 (dbtype=String, size=-1, direction=Input) = null
به عبارتی خروجی مورد انتظار name is null را تولید نکرده است و کوئری ما حداقل با SQL Server نتیجه‌ای را به همراه نخواهد داشت. در مورد Age نیز به همین صورت است.


راه حل:

برای حالت Age، روش زیر خروجی age is null را تولید می‌کند:
 var list2 = ctx.Users.Where(x => !x.Age.HasValue).ToList();
و یا استفاده از object.Equals نیز مشکل را برطرف خواهد کرد:
int? age = null;
var list2 = ctx.Users.Where(x => object.Equals(x.Age, age)).ToList();
برای حالت Name رشته‌ای می‌توان از روش زیر استفاده کرد:
 var list1 = ctx.Users.Where(x => string.IsNullOrEmpty(x.Name)).ToList();
و یا روش کلی‌تر زیر نیز جواب می‌دهد:
string name = null;
var list1 = ctx.Users.Where(x => name == null ? x.Name == null : x.Name == name).ToList();
کاری که در اینجا انجام شده استفاده از x.Name == null در حالت نال بودن name است. از این جهت که EF با کوئری ذیل به علت عدم استفاده از پارامتر برای معرفی مقداری نال، مشکلی ندارد:
 var list1 = ctx.Users.Where(x => x.Name == null).ToList();