۱۳۹۰/۰۳/۰۴

آشنايي با Fluent interfaces


تعريف مقدماتي fluent interface در ويكي پديا به شرح زير است: (+)

In software engineering, a fluent interface (as first coined by Eric Evans and Martin Fowler) is a way of implementing an object oriented API in a way that aims to provide for more readable code.

به صورت خلاصه هدف آن فراهم آوردن روشي است كه بتوان متدها را زنجير وار فراخواني كرد و به اين ترتيب خوانايي كد نوشته شده را بالا برد. پياده سازي آن هم شامل دو نكته است:
الف) نوع متد تعريف شده بايد مساوي با نام كلاس جاري باشد.
ب) در اين حالت خروجي متد‌هاي ما كلمه كليدي this خواهند بود.

براي مثال:
using System;

namespace FluentInt
{
public class FluentApiTest
{
private int _val;

public FluentApiTest Number(int val)
{
_val = val;
return this;
}

public FluentApiTest Abs()
{
_val = Math.Abs(_val);
return this;
}

public bool IsEqualTo(int val)
{
return val == _val;
}
}
}
مثالي هم از استفاده‌ي آن به صورت زير مي‌تواند باشد:
if (new FluentApiTest().Number(-10).Abs().IsEqualTo(10))
{
Console.WriteLine("Abs(-10)==10");
}
كه در آن توانستيم تمام متدها را زنجير وار و با خوانايي خوبي شبيه به نوشتن جملات انگليسي در كنار هم قرار دهيم.
خوب! اين مطلبي است كه همه جا پيدا مي‌كنيد و مطلب جديدي هم نيست. اما موردي را كه سخت مي‌شود يافت اين است كه طراحي كلاس فوق ايراد دارد. براي مثال شما مي‌توانيد تركيب‌هاي زير را هم تشكيل دهيد و كار مي‌كند؛ يا به عبارتي برنامه كامپايل مي‌شود و اين خوب نيست:
if(new FluentApiTest().Abs().Number(-10).IsEqualTo(10)) ...
if (new FluentApiTest().Abs().IsEqualTo(10)) ...
مي‌شود در كدهاي برنامه يك سري throw new exception را هم قرار داد كه ... هي! اول بايد اون رو فراخواني كني بعد اين رو!
ولي ... اين روش هم صحيح نيست. از ابتداي كار نبايد بتوان متد بي‌ربطي را در طول اين زنجيره مشاهده كرد. اگر قرار نيست استفاده گردد، نبايد هم در intellisense ظاهر شود و پس از آن هم نبايد قابل كامپايل باشد.

بنابراين صورت مساله به اين ترتيب اصلاح مي‌شود:
مي‌خواهيم پس از نوشتن FluentApiTest و قرار دادن يك نقطه، در intellisense فقط Number ظاهر شود و نه هيچ متد ديگري. پس از ذكر متد Number فقط متد Abs يا مواردي شبيه به آن مانند Sqrt ظاهر شوند. پس از انتخاب مثلا Abs آنگاه متد IsEqualTo توسط Intellisense قابل دسترسي باشد. در روش اول فوق، به صورت دوستانه همه چيز در دسترس است و هر تركيب قابل كامپايلي را مي‌شود با متدها ساخت كه اين مورد نظر ما نيست.
اينبار پياده سازي اوليه به شرح زير تغيير خواهد كرد:
using System;

namespace FluentInt
{
public class FluentApiTest
{
public MathMethods<FluentApiTest> Number(int val)
{
return new MathMethods<FluentApiTest>(this, val);
}
}

public class MathMethods<TParent>
{
private int _val;
private readonly TParent _parent;

public MathMethods(TParent parent, int val)
{
_val = val;
_parent = parent;
}

public Restrictions<MathMethods<TParent>> Abs()
{
_val = Math.Abs(_val);
return new Restrictions<MathMethods<TParent>>(this, _val);
}
}

public class Restrictions<TParent>
{
private readonly int _val;
private readonly TParent _parent;

public Restrictions(TParent parent, int val)
{
_val = val;
_parent = parent;
}

public bool IsEqualTo(int val)
{
return _val == val;
}
}
}
در اينجا هم به همان كاربرد اوليه مي‌رسيم:
if (new FluentApiTest().Number(-10).Abs().IsEqualTo(10))
{
Console.WriteLine("Abs(-10)==10");
}
با اين تفاوت كه intellisense هربار فقط يك متد مرتبط در طول زنجيره را نمايش مي‌دهد و تمام متدها در همان ابتداي كار قابل انتخاب نيستند.
در پياده سازي كلاس MathMethods از Generics استفاده شده به اين جهت كه بتوان نوع متد Number را بر همين اساس تعيين كرد تا متدهاي كلاس MathMethods در Intellisense (يا به قولي در طول زنجيره مورد نظر) ظاهر شوند. كلاس Restrictions نيز به همين ترتيب معرفي شده است و از آن جهت تعريف نوع متد Abs استفاده كرديم. هر كلاس جديد در طول زنجيره، توسط سازنده خود به وهله‌اي از كلاس قبلي به همراه مقادير پاس شده دسترسي خواهد داشت. به اين ترتيب زنجيره‌اي را تشكيل داده‌ايم كه سازمان يافته است و نمي‌توان در آن متدي را بي‌جهت پيش يا پس از ديگري صدا زد و همچنين ديگر نيازي به بررسي نحوه‌ي فراخواني‌هاي يك مصرف كننده نيز نخواهد بود زيرا برنامه كامپايل نمي‌شود.

۱۳۹۰/۰۳/۰۳

استفاده از 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 باشد.

۱۳۹۰/۰۲/۳۰

مقابله با XSS ؛ يكبار براي هميشه!


ASP.NET به صورت پيش فرض در مقابل ارسال هر نوع تگي عكس العمل نشان مي‌دهد و پيغام خطاي يافتن خطري بالقوه را گوشزد مي‌كند. اما بين خودمان باشد، همه اين قابليت را خاموش مي‌كنند! چون در يك برنامه واقعي نياز است تا مثلا كاربران تگ html هم ارسال كنند. براي نمونه يك اديتور متني پيشرفته را درنظر بگيريد. خاموش كردن اين قابليت هم مساوي است با فراهم كردن امكان ارسال تگ‌هاي مجاز و در كنار آن بي دفاع گذاشتن برنامه در مقابل حملات XSS.
توصيه هم اين است كه همه جا از توابع مثلا HtmlEncode و موارد مشابه حتما استفاده كنيد. ولي باز هم خودمونيم ... چند نفر از شماها اينكار را مي‌كنيد؟!
بهترين كار در اين موارد وارد شدن به pipe line پردازشي ASP.NET و دستكاري آن است! اينكار هم توسط HttpModules ميسر است. به عبارتي در ادامه مي‌خواهيم ماژولي را بنويسيم كه كليه تگ‌هاي ارسالي كوئري استرينگ‌ها را پاك كرده و همچنين تگ‌هاي خطرناك موجود در مقادير ارسالي فرم‌هاي برنامه را هم به صورت خودكار حذف كند. اما هنوز اجازه بدهد تا كاربران بتوانند تگ HTML هم ارسال كنند.
مشكل! در ASP.NET مقادير ارسالي كوئري استرينگ‌ها و همچنين فرم‌ها به صورت NameValueCollection در اختيار برنامه قرار مي‌گيرند و ... خاصيت IsReadOnly اين مجموعه‌ها در حين ارسال، به صورت پيش فرض true است و همچنين غيرعمومي! يعني به همين سادگي نمي‌توان عمليات تميزكاري را روي مقادير ارسالي، پيش از مهيا شدن آن جهت استفاده در برنامه اعمال كرد. بنابراين در ابتداي كار نياز است با استفاده از قابليت Reflection‌ ، اندكي در سازوكار داخلي ASP.NET دست برد، اين خاصيت فقط خواندني غيرعمومي را براي مدت كوتاهي false كرد و سپس مقصود نهايي را اعمال نمود. پياده سازي آن‌ را در ادامه مشاهده مي‌كنيد:
using System;
using System.Collections.Specialized;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using Microsoft.Security.Application;

namespace AntiXssMdl
{
public class AntiXssModule : IHttpModule
{
private static readonly Regex _cleanAllTags = new Regex("<[^>]+>", RegexOptions.Compiled);

public void Init(HttpApplication context)
{
context.BeginRequest += CleanUpInput;
}

public void Dispose()
{ }

private static void CleanUpInput(object sender, EventArgs e)
{
HttpRequest request = ((HttpApplication)sender).Request;

if (request.QueryString.Count > 0)
{
//تميزكاري مقادير كليه كوئري استرينگ‌ها پيش از استفاده در برنامه
CleanUpAndEncode(request.QueryString, allowHtmltags: false);
}

if (request.HttpMethod == "POST")
{
//تميزكاري كليه مقادير ارسالي به سرور
if (request.Form.Count > 0)
{
CleanUpAndEncode(request.Form, allowHtmltags: true);
}
}
}

private static void CleanUpAndEncode(NameValueCollection collection, bool allowHtmltags)
{
//اندكي دستكاري در سيستم داخلي دات نت
PropertyInfo readonlyProperty = collection
.GetType()
.GetProperty("IsReadOnly",
BindingFlags.Instance | BindingFlags.NonPublic);
readonlyProperty.SetValue(collection, false, null);//IsReadOnly=false

for (int i = 0; i < collection.Count; i++)
{
if (string.IsNullOrWhiteSpace(collection[i])) continue;

if (!allowHtmltags)
{
//در حالت كوئري استرينگ دليلي براي ارسال هيچ نوع تگي وجود ندارد
collection[collection.Keys[i]] =
AntiXss.HtmlEncode(_cleanAllTags.Replace(collection[i], string.Empty));
}
else
{
//قصد تميز سازي ويوو استيت را نداريم چون در اين حالت وب فرم‌ها از كار مي‌افتند
if (collection.Keys[i].StartsWith("__VIEWSTATE")) continue;
//در ساير موارد كاربران مجازند فقط تگ‌هاي سالم را ارسال كنند و مابقي حذف مي‌شود
collection[collection.Keys[i]] = Sanitizer.GetSafeHtml(collection[i]);
}
}

readonlyProperty.SetValue(collection, true, null);//IsReadOnly=true
}
}
}

در اين كلاس از كتابخانه AntiXSS مايكروسافت استفاده شده است. آخرين نگارش آن‌را از اينجا دريافت نمائيد. نكته مهم آن متد Sanitizer.GetSafeHtml است. به كمك آن با خيال راحت مي‌توان در يك سايت، از يك اديتور متني پيشرفته استفاده كرد. كاربران هنوز مي‌توانند تگ‌هاي HTML را ارسال كنند؛ اما در اين بين هرگونه سعي در ارسال عبارات و تگ‌هاي حاوي حملات XSS پاكسازي مي‌شود.

و يك وب كانفيگ نمونه براي استفاده از آن به صورت زير مي‌تواند باشد (تنظيم شده براي IIS6 و 7):
<?xml version="1.0"?>
<configuration>
<system.web>
<pages validateRequest="false" enableEventValidation="false" />
<httpRuntime requestValidationMode="2.0" />
<compilation debug="true" targetFramework="4.0" />

<httpModules>
<add name="AntiXssModule" type="AntiXssMdl.AntiXssModule"/>
</httpModules>

</system.web>

<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
<add name="AntiXssModule" type="AntiXssMdl.AntiXssModule"/>
</modules>
</system.webServer>
</configuration>

براي مثال به تصوير زير دقت كنيد. ماژول فوق، فقط تگ‌هاي سبز رنگ را (حين ارسال به سرور) مجاز دانسته، اسكريپت ذيل لينك را كلا حذف كرده و تگ‌هاي موجود در كوئري استرينگ را هم نهايتا (زمانيكه در اختيار برنامه قرار مي‌گيرد) حذف خواهد كرد.


دريافت مثال

۱۳۹۰/۰۲/۲۹

چگونه از SVN جهت به روز رساني يك سايت استفاده كنيم؟


اين سناريو رو در نظر بگيريد:
وب سرور ما در همان محلي قرار دارد كه SVN Server نصب شده است.
مي‌خواهيم به ازاي هربار Commit تيم به مخزن SVN ما، سايت ارائه شده توسط وب سرور نيز به صورت خودكار به روز شود.
چه بايد كرد؟!

احتمالا خيلي‌ها تصور مي‌كنند كه امكان پذير نيست؛ چون مخزن SVN موجود در سرور، ساختار خودش را دارد و همانند فايل‌هاي يك پروژه معمولي نگهداري نمي‌شود.
براي انجام اينكار چندين روش موجود است، كه تمام آن‌ها به مفهوم hooks در SVN گره خورده است. هرچند hook به معناي قلاب است، اما در اينجا معناي تريگر را دارد. شبيه به تريگرهاي SQL Server : پيش يا پس از انجام كار يا رخداد مشخصي، فلان كار را انجام بده. (براي اطلاعات بيشتر مي‌توانيد به فصل hooks در اين كتابچه مراجعه كنيد: (+))
در ميان اين قلاب‌هاي موجود، مي‌توان از قلاب post-commit جهت به روز رساني يك سايت پس از هر هماهنگ سازي با مخزن SVN استفاده كرد. پيشنهاد من به تمام كساني كه مي‌خواهند كار با SVN را شروع كنند استفاده از برنامه رايگان Visual SVN Server است. اين برنامه سازگاري فوق العاده‌اي با محيط ويندوز دارد (از لحاظ تعريف سطح دسترسي‌ها). همچنين تعريف hooks را هم به شدت ساده كرده است. فقط كافي است روي يك مخزن كد تعريف شده در Visual SVN Server كليك راست كرده و در برگه‌ي باز شده، تنظيمات سطوح دسترسي يا تعاريف Hooks را اضافه نمود (در اينجا اعمال سطوح دسترسي روي پوشه‌ها يا روي فايل‌ها نيز به همان شكل با كليك راست و كم و زياد كردن كاربران ميسر است؛ همانند دادن دسترسي بر اساس امكانات NTFS و اكتيودايركتوري).

بنابراين به صورت خلاصه:
  • فرض بر اين است كه مخزن كد SVN ايي را بر روي سرور راه اندازي كرده‌ايد. همچنين پوشه‌اي را كه مي‌خواهيد ريشه سايت باشد، مثلا در مسير دلخواه C:\path\www قرار دارد.
  • براي شروع كار، check out بايد صورت گيرد. يا مي‌توان از TortoiseSVN استفاده كرد يا چون مخزن كد در همان سرور است، دستور زير نيز كار مي‌كند:
svn checkout file:///c:/svn/MyRepository/trunk C:\path\www
  • سپس يك فايل bat بايد درست كنيد با محتواي زير:
svn update file:///c:/svn/MyRepository/trunk C:\path\www

اين فايل bat بايد در همان قسمت تعريف post-commit hook استفاده شود.
به اين معنا كه پس از هر commit ، لطفا مسير C:\path\www را بر اساس آخرين به روز رساني‌هاي مخزن كد به صورت خودكار به روز كن. در اين حالت اگر فايلي حذف شده باشد، به صورت خودكار از ريشه سايت شما حذف مي‌شود و اگر فايل يا فايل‌هايي تغيير كرده باشند نيز سريعا به روز رساني آن‌ها انجام خواهد شد.
در روش svn update ، پوشه‌هاي مخفي svn نيز در ريشه سايت حضور خواهند داشت. وجود آن‌ها هم الزامي است زيرا update بر همين اساس كار مي‌كند.
  • اگر مي‌خواهيد اين پوشه‌هاي مخفي وجود نداشته باشند از دستور svn export استفاده كنيد. فقط دقت كنيد كه در اين حالت اگر فايلي از مخزن كد حذف شده باشد، باز هم در ريشه سايت وجود خواهد داشت. راه حلي هم كه توصيه شده، اين است كه در همان bat فايلي كه درست مي‌كنيد ابتدا دستور حذف محتويات پوشه ريشه را صادر كنيد و بعد svn export . البته بديهي است اين روش نسبت به svn update كندتر است و svn update به شدت بهينه و سريع مي‌باشد.
  • يا راه ديگر بجاي حذف كردن پوشه موجود و بعد export به آن، استفاده از برنامه‌هايي مانند Robocopy است كه مي‌توانند عمليات همگام سازي را هم انجام دهند. در اين حالت محتواي فايل bat شما شبيه به دستورات زير خواهد شد:
svn checkout file:///c:/svn/MyRepository/trunk C:\temp\Site1 >> output.log
robocopy C:\temp\Site1 C:\path\www *.* /S /XF *.cs *.tmp *.sln *.csproj *.webinfo /XD .svn _svn /PURGE >> output.log

به اين معنا كه پس از هر commit‌ به مخزن كد (با توجه به تعريف قلاب ذكر شده)، ابتدا يك svn checkout در يك پوشه موقتي (خارج از ريشه اصلي سايت) انجام گرديده و سپس برنامه robocopy يا موارد مشابه آن وارد عمل شده و تغييرات را با ريشه اصلي هماهنگ مي‌كنند (در اينجا مي‌توان مشخص كرد چه فايل‌هايي با پسوندهاي مشخص، با ريشه سايت هماهنگ نشوند).

در كل همان روش svn update به نظر سريعتر و مقرون به صرفه‌تر است. اگر از IIS استفاده مي‌كنيد، به صورت پيش فرض كسي نمي‌تواند محتواي پوشه‌اي را با وارد كردن آدرس آن در مرورگر بررسي كند، همچنين IIS فايل‌هايي را كه نمي‌شناسد (پسوند از پيش تعريف شده‌اي در بانك اطلاعاتي آن ندارند)، سرو نمي‌كند و در صورت درخواست آن‌ها، خطاي 404 يا "پيدا نشد" به كاربر نهايي ارائه خواهد شد.

۱۳۹۰/۰۲/۲۷

QueryOver Extensions


جهت تكميل مطلب قبل (+)، مي‌توان به ازاي تمام توابع SQL موجود و همچنين تمام حالت‌هاي اعمال محدوديت مانند مساوي، بزرگتر، كوچكتر و امثال آن، extension method نوشت. يا اينكه يك متد داشت كه بتوان پارامترهاي آن را تنظيم كرد. به همين جهت كتابخانه زير را تهيه كرده‌ام كه از آدرس زير قابل دريافت است:



نحوه استفاده:
ابتدا بايد به NH معرفي شود (يكبار در ابتداي كار برنامه):
RegistrExt.RegistrMyQueryOverExts();
سپس استفاده از آن به سادگي زير خواهد بود:
using QueryOverSqlFuncsExts;

var data = session.QueryOver<Account>()
.Where(x => x.Name.Evaluate(new SqlFunc().CharIndex("a", 1).IsEqualTo(2)))
.List();
مثال‌هاي بيشتر را در پوشه تست پروژه مي‌توانيد پيدا كنيد.

۱۳۹۰/۰۲/۲۴

QueryOver در NHibernate و تفاوت‌هاي آن با LINQ to NH


در NHibernate چندين و چند روش، جهت تهيه كوئري‌ها وجود دارد كه QueryOver يكي از آن‌ها است (+). QueryOver نسبت به LINQ to NH سازگاري بهتري با ساز و كار دروني NHibernate دارد؛ براي مثال امكان يكپارچگي آن با سطح دوم كش. هر چند ظاهر QueryOver با LINQ يكي است، اما در عمل متفاوتند و راه و روش خاص خودش را طلب مي‌كند. براي مثال در LINQ to NH مي‌تواند نوشت x.Property.Contains اما در QueryOver متدي به نام contains قابل استفاده نيست (هر چند در Intellisense ظاهر مي‌شود اما عملا تعريف نشده است و نبايد آن‌را با LINQ اشتباه گرفت) و سعي در استفاده از آن‌ها به استثناهاي زير ختم مي‌شوند:
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
براي مثال كلاس زير را در نظر بگيريد؛ كوئري‌هاي مطلب جاري بر اين اساس تهيه خواهند شد:
using NHibernate.Validator.Constraints;
using System;

namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }

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

[NotNull]
public virtual int Balance { set; get; }

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

1) يافتن ركوردهايي كه در يك مجموعه‌ي مشخص قرار دارند. براي مثال balance آن‌ها مساوي 10 و 12 است:
var list = new[]  { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)

2) پياده سازي همان متد Contains ذكر شده، در QueryOver:
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */

در اينجا بر اساس مقادير مختلف MatchMode مي‌توان متدهاي StartsWith (MatchMode.Start) ، EndsWith (MatchMode.End) ، Equals (MatchMode.Exact) را نيز تهيه نمود.

انجام مثال دوم راه ساده‌تري نيز دارد. قسمت WhereRestrictionOn و IsLike به صورت يك سري extension متد ويژه در فضاي نام NHibernate.Criterion تعريف شده‌اند. ابتدا اين فضاي نام را به كلاس جاري افزوده و سپس مي‌توان نوشت :
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();

اين فضاي نام شامل چهار extension method به نام‌هاي IsLike ، IsInsensitiveLike ، IsIn و IsBetween است.


چگونه extension method سفارشي خود را تهيه كنيم؟

بهترين كار اين است كه به سورس NHibernate ، فايل‌هاي RestrictionsExtensions.cs و ExpressionProcessor.cs كه تعاريف متد IsLike در آن‌ها وجود دارد مراجعه كرد. در اينجا مي‌توان با نحوه‌ي تعريف و سپس ثبت آن در رجيستري extension methods مرتبط با QueryOver توسط متد عمومي RegisterCustomMethodCall آشنا شد. در ادامه سه كار را مي‌توان انجام داد:
-متد مورد نظر را در كدهاي خود (نه كدهاي اصلي NH) اضافه كرده و سپس با فراخواني RegisterCustomMethodCall آن‌را قابل استفاده نمائيد.
-متد خود را به سورس اصلي NH اضافه كرده و كامپايل كنيد.
-متد خود را به سورس اصلي NH اضافه كرده و كامپايل كنيد (بهتر است همان روش نامگذاري بكار گرفته شده در فايل‌هاي ذكر شده رعايت شود). يك تست هم براي آن بنويسيد (تست نويسي هم يك سري اصولي دارد (+)). سپس يك patch از آن روي آن ساخته (+) و براي تيم NH ارسال نمائيد (تا جايي كه دقت كردم از كليه ارسال‌هايي كه آزمون واحد نداشته باشند، صرفنظر مي‌شود).

مثال:
مي‌خواهيم extension متد جديدي به نام YearIs را به QueryOver اضافه كنيم. اين متد را هم بر اساس توابع توكار بانك‌هاي اطلاعاتي، تهيه خواهيم نمود. ليست كامل اين نوع متدهاي بومي SQL را در فايل Dialect.cs سورس‌هاي NH مي‌توان يافت (البته به صورت پيش فرض از متد extract براي جداسازي قسمت‌هاي مختلف تاريخ استفاده مي‌كند. اين متد در فايل‌هاي Dialect مربوط به بانك‌هاي اطلاعاتي مختلف، متفاوت است و برحسب بانك اطلاعاتي جاري به صورت خودكار تغيير خواهد كرد).
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;

namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}

public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}

public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}

اكنون براي استفاده خواهيم داشت:
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //يكبار در ابتداي اجراي برنامه بايد ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();

براي مثال اگر بانك اطلاعاتي انتخابي از نوع SQLite باشد، خروجي SQL مرتبط به شكل زير خواهد بود:
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */


هر چند ما تابع year را در متد ProcessAnsiYear ثبت كرده‌ايم اما بر اساس فايل SQLiteDialect.cs ، تعاريف مرتبط و مخصوص اين بانك اطلاعاتي (مانند متد strftime فوق) به صورت خودكار دريافت مي‌گردد و كد ما مستقل از نوع بانك اطلاعاتي خواهد بود.


نكته جالب!
LINQ to NH هم قابل بسط است؛ كاري كه در ORM هاي ديگر به اين سادگي نيست. چند مثال در اين زمينه:
چگونه تابع سفارشي SQL Server خود را به صورت يك extension method تعريف و استفاده كنيم: (+) ، يك نمونه ديگر: (+) و نمونه‌اي ديگر: (+).

۱۳۹۰/۰۲/۱۶

سرنوشت Mono در هاله‌اي از ابهام


همانطور كه مطلع هستيد، شركت Attachmate، شركت ناول را مدتي قبل خريد. در اين بين، شركت Attachmate قصد دارد SUSE Linux را ادامه داده اما پروژه Mono را خاتمه دهد. متن زير خلاصه‌اي از مصاحبه با Jeff Hawn، مدير شركت Attachmate است:
"واحد مركزي پروژه SUSE به آلمان (هامبورگ) منتقل شده و تصميمات اصلي مرتبط با مباحث توسعه لينوكس (كه قبلا توسط تيم ناول صورت مي‌گرفت)، توسط واحد هامبورگ انجام خواهد شد. به اين ترتيب اين واحد تصميم گرفته است تا تعداد زيادي از كاركنان آمريكايي ناول را بازخريد كند (حدود 800 نفر، يا 50 درصد كارمندان آمريكايي پيشين ناول را)."

در اين بين دقيقا مشخص نشده كه تكليف "Miguel de Icaza" - مدير تيم Mono - چه شده است. حدس زده مي‌شود كه تيم سي نفره Mono هم جزو اين 800 نفر كارمند آمريكايي بازخريد شده باشند (و آقاي de Icaza هم تاكنون در بلاگ خود مطلبي را در اين مورد منتشر نكرده و ترجيح داده است كه سكوت كند).
در كل پاياني را براي پروژه‌هاي سورس باز نمي‌توان متصور شد؛ اما بديهي است سرعت توسعه مداوم آن كاهش خواهد يافت.
جالب است بدانيد Banshee كه مدياپليري است مبتني بر Mono ، به عنوان مدياپلير پيش فرض Ubuntu-11.04 مورد استفاده قرار گرفته است.


نظر يكي از خوانندگان مطلب ZDNet فوق هم در اين مورد جالب بود:
مونو، توسعه تحت لينوكس را از درد به لذت تبديل كرده است! سي پلاس پلاس زبان بزرگي است اما سرعت توسعه با مونو 20 برابر زماني است كه بخواهيد با ++C كار كنيد.

۱۳۹۰/۰۲/۱۳

تهيه يك Clone از مخزن كدي در گوگل كد


براي مثال پروژه "unhaddins" را در نظر بگيريد. اين پروژه يك سري افزونه را جهت كار ساده‌تر با NHibernate ارائه داده است. براي مثال چگونه با WPF يا WCF و امثال آن بتوان به سادگي با NHibernate ارتباط برقرار كرد. اين پروژه خروجي قابل دريافتي ندارد؛ به عبارتي يك سري سورس كد است. دريافت يك مخزن كد هم كه از گوگل كد در اين سمت مشكل است ... اما راه بهتري هم وجود دارد. يكي از خواص كار با سورس كنترل‌ها، امكان تهيه يك clone از يك مخزن كد است. تمام پروژه‌هاي موجود در گوگل كد هم به اين شكل با SVN در دسترس هستند:
http://someproject.googlecode.com/svn/trunk/
كه به جاي someproject ، نام پروژه مورد نظر قرار خواهد گرفت.

براي نمونه، در سايت https://bitbucket.org ثبت نام كنيد. سپس گزينه ايجاد يك مخزن جديد را انتخاب كرده:



و در صفحه‌ي باز شده، گزينه‌ي Import from Subversion را انتخاب كنيد:



در اينجا Url خواسته شده بايد شبيه به همان آدرس trunk فوق باشد و اگر تيك private فعال باشد (كه هست)،‌ ديگران امكان دسترسي به مخزن كد شما را نخواهند داشت. البته اين تنظيم پس از دريافت، در برگه‌ي Admin مخزن ايجاد شده نيز قابل تغيير است.

به علاوه سايت github.com هم هر چند بر اساس Git كار مي‌كند، اما امكان تهيه يك كپي مطابق اصل از يك مخزن كد SVN را هم دارد؛ به شرح زير:

يك اكانت رايگان در GitHub درست كنيد. بعد يك مخزن خالي جديد را ايجاد كرده و در همان صفحه روي لينك Import a Subversion Repository كليك كنيد و آدرس svn مورد نظر را بدهيد.

البته GitHub در دريافت پروژه unhaddins موفق عمل نكرد، اما bitbucket خيلي سريع كل آن‌را دريافت نمود.

۱۳۹۰/۰۲/۱۱

فعال سازي سطح دوم كش در Fluent NHibernate


سطح اول كش در NHibernate در يك تراكنش معنا پيدا مي‌كند (+)؛ اما نتايج حاصل از اعمال سطح دوم (+) آن، در اختيار تمام تراكنش‌هاي جاري برنامه خواهند بود. در ادامه قصد داريم نحوه فعال سازي سطح دوم كش NHibernate را توسط Fluent NHibernate بررسي كنيم.

الف) دريافت كش پروايدر
براي اين منظور به صفحه اصلي آن در سايت سورس فورج مراجعه نمائيد(+). اگر به علت تحريم‌ها امكان دريافت فايل‌هاي مرتبط را نداشتيد از اين برنامه استفاده كنيد(+). پس از دريافت، مي‌خواهيم نحوه فعال سازي NHibernate.Caches.SysCache.dll را بررسي كنيم (اين اسمبلي، در برنامه‌هاي وب و دسكتاپ بدون مشكل كار مي‌كند).

ب) اعمال به قسمت تعاريف اوليه
پس از دريافت اسمبلي NHibernate.Caches.SysCache.dll و افزودن ارجاعي به آن، اكنون نوبت به معرفي آن به تنظيمات Fluent NHibernate‌ مي‌باشد. اين‌كار هم بسيار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...

ج) تعريف نوع كش در هنگام ايجاد نگاشت‌ها
اگر از ClassMap‌ها براي تعريف نگاشت‌ها استفاده مي‌كنيد، در انتهاي تعاريف يك سطر Cache.ReadWrite را اضافه كنيد.
اگر از AutoMapping استفاده مي‌كنيد، نياز است تا با استفاده از IAutoMappingOverride (+) سطر ياد شده اضافه گردد؛ براي مثال:
using FluentNHibernate.Automapping.Alterations;

namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
تعريف يك سطر فوق هم مهم است؛ زيرا در غيراينصورت فقط primary key حاصل از بار اول فراخواني كوئري‌هاي مرتبط كش مي‌شوند؛ نه نتيجه عمليات. هرچند اين مورد هم يك قدم مثبت به شمار مي‌رود از اين لحاظ كه براي مثال تهيه نتايج كوئري بر روي فيلدي كه ايندكس بر روي آن تعريف نشده است هميشه از حالت تهيه كوئري بر روي فيلد داراي ايندكس كندتر است. اما هدف ما در اينجا اين است كه پس از بار اول فراخواني كوئري، بار‌هاي دوم و بعدي ديگر كوئري خاصي را به بانك اطلاعاتي ارسال نكرده و نتايج از كش خوانده شوند (جهت استفاده عموم كاربران در كليه تراكنش‌هاي جاري برنامه).

د) اعمال متد Cacheable به كوئر‌ي‌ها
سه مرحله قبل نحوه برپايي مقدماتي سطح دوم كش را بيان مي‌كنند و تنها يكبار نياز است انجام شوند. در ادامه هر جايي كه نياز داشتيم نتايج كوئري مورد نظر كش شوند (و بايد دقت داشت كه اين كش شدن سطح دوم به معني در دسترس بودن نتايج آن جهت تمام كاربران برنامه در تمام تراكنش‌هاي جاري برنامه هستند؛ براي مثال نتايج آمار سايت كه دسترسي عمومي دارد) تنها كافي است متد Cacheable را به كوئري مورد نظر اضافه كرد؛ براي مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();

ه) چگونه صحت اعمال سطح دوم كش را بررسي كنيم؟
براي بررسي اين امر بايد به خروجي SQL نهايي مراجعه كرد (+). سه تراكنش مجزا را تعريف كنيد. در تراكنش اول يك insert ساده، در تراكنش دوم كوئري از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراكنش سوم مجددا همان كوئري تراكنش دوم را (به همراه اعمال متد Cacheable) تكرار كنيد. حاصل كار تنها بايد دو عبارت SQL باشند. يك مورد جهت insert و يك مورد هم select . در تراكنش سوم، از نتايج كش شده تراكنش دوم استفاده خواهد شد؛ به همين جهت ديگري كوئري سومي به بانك اطلاعاتي ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش كنيد، سه كوئري را مشاهده خواهيد كرد، اما كوئري سوم با كوئري دوم اندكي متفاوت خواهد بود و بهينه‌تر؛ چون به صورت هوشمند بر اساس جستجوي بر روي primary key تغيير كرده است (صرفنظر از اينكه قسمت where كوئري شما چيست).