‏نمایش پست‌ها با برچسب LINQ. نمایش همه پست‌ها
‏نمایش پست‌ها با برچسب LINQ. نمایش همه پست‌ها

۱۳۹۰/۰۸/۲۳

تهيه گزارشات Crosstab به كمك LINQ - قسمت دوم


اگر به قسمت اول «تهيه گزارشات Crosstab به كمك LINQ» دقت كرده باشيد، يك مشكل كوچك دارد و آن هم لزوم مشخص سازي دقيق ستون‌هايي است كه مي‌خواهيم در گزارش ظاهر شوند. مثلا دقيقا مشخص كنيم كه نام واحد چيست يا دقيقا روز را مشخص كنيم. اين مورد براي گزارش‌هاي كوچك مشكلي ندارد؛ ولي اگر همان مثال دوم را در نظر گرفته و بازه را كمي بيشتر كنيم، مثلا يك ماه، آن وقت بايد حداقل 30 بار بنويسيم Day1IsPresent تا ... Day30IsPresent و يا اگر بازه‌ي گزارشگيري به اختيار كاربر باشد آن وقت چه بايد كرد؟ مثلا يكبار 7 روز پايان ماه را انتخاب كند، يكبار 14 روز را، شايد يك بار هم مثلا 90 روز را مد نظر داشته باشد (تعداد ستون‌ها متغير باشد يا به عبارتي Dynamic Crosstab نياز است ايجاد شود).
براي حل اين مساله، مي‌توان از متد الحاقي زير از سايت extensionmethod.net كمك گرفت:

using System;
using System.Collections.Generic;
using System.Linq;

namespace PivotExtensions
{
    public static class Ext
    {
        public static Dictionary<TKey1, Dictionary<TKey2, TValue>>
                        Pivot<TSource, TKey1, TKey2, TValue>
                        (
                            this IEnumerable<TSource> source,
                            Func<TSource, TKey1> key1Selector,
                            Func<TSource, TKey2> key2Selector,
                            Func<IEnumerable<TSource>, TValue> aggregate
                        )
        {
            return source.GroupBy(key1Selector)
                         .Select(
                            key1Group => new
                                {
                                    Key = key1Group.Key,
                                    Value = key1Group.GroupBy(key2Selector)
                                         .Select(
                                            key2Group => new
                                               {
                                                   K = key2Group.Key,
                                                   V = aggregate(key2Group)
                                               })
                                         .ToDictionary(e => e.K, o => o.V)
                                })
                         .ToDictionary(e => e.Key, o => o.Value);
        }
    }
}

در اين متد:
key1Selector مشخص كننده ستون‌هاي ثابت و مشخص سمت راست يا چپ (بر اساس جهت صفحه) گزارش است. در سيستم‌هاي مختلف اين ستون‌ها نام‌هايي مانند keyColumn ، leftColumn و Row Heading ممكن است داشته باشند.
key2Selector ستون‌هاي پوياي گزارش را تشكيل مي‌دهد. در ساير سيستم‌ها اين پارامتر، pivotNameColumn ،VariableColumn ، topField و يا Column Heading هم ناميده مي‌شود.
Aggregate در اينجا مشخص مي‌كند كه مقادير ستون‌هاي پوياي ياد شده چگونه بايد محاسبه شوند.

با توجه به اين متد، براي نمونه جهت حل مثال اول قسمت قبل خواهيم داشت:

var list = ExpenseDataSource.ExpensesDataSource();
var pivotList = list.Pivot(
                x =>
                    new
                    {
                        x.Date.Year,
                        x.Date.Month
                    },
                  x1 => x1.Department,
                  x2 => x2.Sum(x => x.Expenses));

با خروجي


فايل LINQPad آن از اينجا قابل دريافت است.


و براي حل مثال دوم قسمت قبل مي‌توان نوشت:

var list2 = StudentsStatDataSource.CreateWeeklyReportDataSource();
var lst = list2.Pivot(
                x =>
                    new 
                    {
                        x.Id,
                        x.Name
                    },
              x1 => "Day " + x1.Date.Day,
              x2 => x2.First().IsPresent);

با خروجي


فايل LINQPad آن از اينجا قابل دريافت است.

۱۳۹۰/۰۸/۲۲

تهيه گزارشات Crosstab به كمك LINQ


در گزارشات Crosstab، رديف‌هاي يك گزارش، تبديل به ستون‌هاي آن مي‌شوند؛ به همين جهت به آن‌ها Pivot tables هم مي‌گويند.
براي مثال فرض كنيد كه قصد داريد گزارش تعداد ساعت كاركرد را به ازاي هر پروژه در طول چند ماه تعيين كنيد. گزارش متداول از اين نوع اطلاعات، يك ليست بلند بالاي بي‌مفهوم است. اين گزارش تشكيل شده از صدها ركورد به ازاي كاركنان مختلف در پروژه‌هاي مختلف و ... هيچ ارزش آماري خاصي ندارد. يك گزارش بدوي است. زمانيكه اين گزارش را تبديل به حالت crosstab مي‌كنيم، اولين ستون فقط يك شماره پروژه خواهد بود و ستون‌هاي بعدي، مثلا نام ماه‌ها و مقادير آن‌ها هم جمع كاركرد افراد بر روي يك پروژه مشخص.

مثال اول) تهيه گزارش Crosstab جمع هزينه‌هاي واحدهاي مختلف به تفكيك ماه

كلاس هزينه‌هاي زير را در نظر بگيريد كه به كمك آن مي‌توان به ازاي هر واحد يا دپارتمان در تاريخ‌هاي متفاوت، هزينه‌اي را مشخص ساخت:

using System;

namespace Pivot.Sample1
{
    public class Expense
    {
        public DateTime Date { set; get; }
        public string Department { set; get; }
        public decimal Expenses { set; get; }
    }
}

با توجه به اين كلاس، يك منبع داده آزمايشي جهت تهيه گزارشات، مي‌تواند به صورت زير باشد:

using System;
using System.Collections.Generic;

namespace Pivot.Sample1
{
    public class ExpenseDataSource
    {
        public static IList<Expense> ExpensesDataSource()
        {
            return new List<Expense>
            {
                new Expense { Date = new DateTime(2011,11,1), Department = "Computer", Expenses = 100 },
                new Expense { Date = new DateTime(2011,11,1), Department = "Math", Expenses = 200 },
                new Expense { Date = new DateTime(2011,11,1), Department = "Physics", Expenses = 150 },

                new Expense { Date = new DateTime(2011,10,1), Department = "Computer", Expenses = 75 },
                new Expense { Date = new DateTime(2011,10,1), Department = "Math", Expenses = 150 },
                new Expense { Date = new DateTime(2011,10,1), Department = "Physics", Expenses = 130 },

                new Expense { Date = new DateTime(2011,9,1), Department = "Computer", Expenses = 90 },
                new Expense { Date = new DateTime(2011,9,1), Department = "Math", Expenses = 95 },
                new Expense { Date = new DateTime(2011,9,1), Department = "Physics", Expenses = 100 }
            };
        }
    }
}

و اگر اين ليست را به همين شكلي كه هست نمايش دهيم، خروجي زير را خواهيم داشت:


كه ... خروجي مطلوبي نيست. در اينجا ما فقط 9 ركورد داريم؛ اما در عمل به ازاي هر روز، يك ركورد مي‌تواند وجود داشته باشد و اين ليست طولاني، هيچ ارزش آماري خاصي ندارد. مي‌خواهيم سرستون‌هاي گزارش ما مطابق جدول زير باشند:


يعني اگر سه ماه را در نظر بگيريم با هر تعداد ركورد، فقط سه رديف به ازاي هر ماه بايد حاصل شود و ستون‌هاي ديگر هم نام بخش‌ها يا واحدهاي موجود باشند.
براي رسيدن به اين خروجي Crosstab، مي‌توان كوئري LINQ زير را به كمك امكانات گروه بندي اطلاعات آن تهيه كرد:

using System.Collections;
using System.Linq;

namespace Pivot.Sample1
{
    public class PivotTable
    {
        public static IList ExpensesCrossTab()
        {
            return ExpenseDataSource
                        .ExpensesDataSource()
                        .GroupBy(t =>
                                   new
                                   {
                                       Year = t.Date.Year,
                                       Month = t.Date.Month
                                   })
                        .Select(myGroup =>
                                   new
                                   {
                                       //Year = myGroup.Key.Year,
                                       Month = myGroup.Key.Month,
                                       ComputerDepartment = myGroup.Where(x => x.Department == "Computer").Sum(x => x.Expenses),
                                       MathDepartment = myGroup.Where(x => x.Department == "Math").Sum(x => x.Expenses),
                                       PhysicsDepartment = myGroup.Where(x => x.Department == "Physics").Sum(x => x.Expenses)
                                   })
                        .ToList();
        }
    }
}

كه اينبار خروجي زير را توليد مي‌كند.


اگر علاقمند باشيد كه مثال فوق را در برنامه‌ي LINQPad آزمايش كنيد، اين فايل را دريافت نموده و در آن برنامه باز نمائيد.


مثال دوم) تهيه ليست Crosstab حضور و غياب افراد در طول يك هفته

كلاس StudentStat را جهت ثبت اطلاعات حضور يك دانشجو، مي‌توان به شكل زير تعريف كرد:

using System;

namespace Pivot.Sample2
{
    public class StudentStat
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public DateTime Date { set; get; }
        public bool IsPresent { set; get; }
    }
}

و بر همين اساس يك منبع داده فرضي جهت انجام گزارشات مي‌تواند به نحو زير تهيه شود:

using System;
using System.Collections.Generic;

namespace Pivot.Sample2
{
    public class StudentsStatDataSource
    {
        public static IList<StudentStat> CreateMonthlyReportDataSource()
        {
            var result = new List<StudentStat>();
            var rnd = new Random();

            for (int day = 1; day < 6; day++)
            {
                for (int student = 1; student < 6; student++)
                {
                    result.Add(new StudentStat
                    {
                        Id = student,
                        Date = new DateTime(2011, 11, day),
                        IsPresent = rnd.Next(-1, 1) == 0 ? true : false,
                        Name = "student " + student
                    });
                }
            }

            return result;
        }
    }
}

خروجي اين گزارش هم در اين حالت ساده با 5 دانشجو و فقط 5 روز، 25 ركورد خواهد بود:


كه ... اين هم آنچنان از لحاظ آماري مطلوب و مفهوم نيست. مي‌خواهيم سطرهاي اين گزارش همانند ليست واقعي حضورغياب، فقط از نام افراد تشكيل شود و همچنين ستون‌ها مثلا شماره يا نام روزهاي يك هفته يا ماه باشند. مثلا به شكل زير:


براي رسيدن به اين خروجي Crosstab، مثلا مي‌توان از كوئري LINQ زير كمك گرفت كه بر اساس شماره دانشجويي اطلاعات را گروه بندي كرده است:

using System.Collections;
using System.Linq;

namespace Pivot.Sample2
{
    public class PivotTable
    {
        public static IList StudentsStatCrossTab()
        {
            return StudentsStatDataSource
                        .CreateWeeklyReportDataSource()
                        .GroupBy(x =>
                                  new
                                  {
                                      x.Id
                                  })
                        .Select(myGroup =>
                                  new
                                  {
                                      myGroup.Key.Id,
                                      Name = myGroup.First().Name,
                                      Day1IsPresent = myGroup.Where(x => x.Date.Day == 1).First().IsPresent,
                                      Day2IsPresent = myGroup.Where(x => x.Date.Day == 2).First().IsPresent,
                                      Day3IsPresent = myGroup.Where(x => x.Date.Day == 3).First().IsPresent,
                                      Day4IsPresent = myGroup.Where(x => x.Date.Day == 4).First().IsPresent,
                                      Day5IsPresent = myGroup.Where(x => x.Date.Day == 5).First().IsPresent,
                                      PresentsCount = myGroup.Where(x => x.IsPresent).Count(),
                                      AbsentsCount = myGroup.Where(x => !x.IsPresent).Count()
                                  })
                        .ToList();
        }
    }
}

و اين كوئري خروجي زير را توليد مي‌كند كه از هر لحاظ نسبت به ليست قبلي مفهوم‌تر است:


فايل LINQPad اين مثال را مي‌توانيد از اينجا دريافت كنيد.

۱۳۹۰/۰۷/۰۱

گوگل ريدر و افزودن توضيحات


اگر به گوگل ريدر دقت كرده باشيد، دو گزينه‌ي به اشتراك گذاري دارد: share و share with note .


اگر گزينه‌ي share with note را انتخاب كرده و توضيحي را ارسال يا اضافه كنيم، اين توضيحات، به فيد از نوع Atom اشتراك‌ها هم اضافه مي‌شود. مثلا:

<?xml version="1.0"?>
<feed xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:gr="http://www.google.com/schemas/reader/atom/" 
   xmlns:idx="urn:atom-extension:indexing" 
   xmlns="http://www.w3.org/2005/Atom" 
   idx:index="no" 
   gr:dir="ltr">

 ...
   
  <entry gr:crawl-timestamp-msec="1316627782108">
    ...
    <gr:annotation>
      <content type="html">text-text-text</content>
      <author>
        <name>Vahid</name>
      </author>
    </gr:annotation>
 ...
  </entry>
  
    ...
  
</feed>



اين افزونه استاندارد نيست و همانطور كه در قسمت xmlns:gr اطلاعات فوق مشخص است، در فضاي نام http://www.google.com/schemas/reader/atom/ معنا پيدا مي‌كند. از دات نت سه و نيم به بعد هم كلاسي جهت خواندن فيدهاي استاندارد وجود دارد (تعريف شده در فضاي نام System.ServiceModel.Syndication). اما چگونه مي‌توان اين افزونه‌ي غير استاندارد را با كمك امكانات توكار دات نت خواند؟
روش كار با استفاده از ElementExtensions هر آيتم يك فيد است؛ به صورت زير :

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Xml;
using System.Xml.Linq;

namespace Linq2Rss
{
    public class RssEntry
    {
        public string Title { set; get; }
        public string Description { set; get; }
        public string Link { set; get; }
        public DateTime PublicationDate { set; get; }
        public string Author { set; get; }
        public string BlogName { set; get; }
        public string BlogAddress { set; get; }
        public string Annotation { set; get; }
    }

    public static class AtomReader
    {
        private static string getAtomAnnotation(this SyndicationElementExtensionCollection items)
        {
            if (!items.Any()) return string.Empty;
            var item = items.Where(x => x.OuterName.ToLowerInvariant() == "annotation").FirstOrDefault();
            if (item == null) return string.Empty;

            var element = item.GetObject<XElement>();
            var content = element.Element("{http://www.w3.org/2005/Atom}content");
            return content == null ? string.Empty : content.Value;
        }

        public static IList<RssEntry> GetEntries(string feedUrl)
        {
            using (var reader = XmlReader.Create(feedUrl))
            {
                var feed = SyndicationFeed.Load(reader);
                if (feed == null) return null;

                return feed.Items.Select(x =>
                    new RssEntry
                    {
                        Title = x.Title.Text,
                        Author = x.Authors.Any() ? x.Authors.First().Name : string.Empty,
                        Description = x.Content == null ? string.Empty : ((TextSyndicationContent)x.Content).Text,
                        Link = x.Links.Any() ? x.Links.First().Uri.AbsoluteUri : string.Empty,
                        PublicationDate = x.PublishDate.UtcDateTime,
                        BlogName = x.SourceFeed.Title.Text,
                        BlogAddress = x.SourceFeed.Links.Any() ? x.SourceFeed.Links.First().Uri.AbsoluteUri : string.Empty,
                        Annotation = x.ElementExtensions.getAtomAnnotation()

                    }).ToList();
            }
        }
    }
}

در اين مثال به كمك متد الحاقي getAtomAnnotation، مجموعه‌ي SyndicationElementExtensionCollection هر آيتم يك فيد بررسي شده، در بين اين‌ها، موردي كه از نوع annotation باشد انتخاب و سپس content آن استخراج مي‌گردد.


نكته‌اي ديگر:
اكثر كلاس‌هاي موجود در فضاهاي نام مرتبط با XML در دات نت امكان خواندن اطلاعات را از يك Uri هم دارند؛ مانند مثال فوق و متد XmlReader.Create بكارگرفته شده در آن. اما اگر بخواهيم حين خواندن اطلاعات، يك پروكسي را نيز به پروسه جاري اضافه كنيم، به نظر خاصيت يا متدي جهت انجام اينكار وجود ندارد. براي رفع اين مشكل مي‌توان يك پروكسي سراسري را تعريف كرد. تنها كافي است خاصيت System.Net.WebRequest.DefaultWebProxy مقدار دهي شود. پس از آن به صورت خودكار بر روي كل برنامه تاثير خواهد گذاشت.


۱۳۸۹/۰۸/۲۲

نحوه‌ي صحيح فراخواني SQL Aggregate Functions حين استفاده از LINQ - قسمت دوم


در قسمت قبل در مورد حالتي كه كوئري انجام شده نتيجه‌اي را بر نگردانده است، بحث شد. در اين قسمت يكي از شايع‌ترين مشكلات حين كار با تابع Sum بررسي خواهد شد.

ابتدا جدول ساده Transactions را با دو فيلد Id و Amount مطابق تصوير زير در نظر بگيريد:


تعدادي ركورد در اين جدول ثبت شده‌اند. اكنون مي‌خواهيم جمع آن‌ها را محاسبه كنيم:


همانطور كه ملاحظه مي‌نمائيد اين عمليات ميسر نيست، زيرا حاصل نهايي فراتر از بازه‌ي تعريف شده‌ي Int32 است.
براي رفع اين مشكل بايد Amount را تبديل به BigInt (براي مثال مرتبط با نگارش‌هاي مختلف SQL Server) كرد. مطابق توضيحات قسمت قبل، اين عمليات casting بايد به lambda expression تعريف شده اعمال گردد، زيرا خروجي Sum بر مبناي آن تعيين مي‌گردد.



در اين حالت خروجي SQL آن نيز به صورت زير در خواهد آمد:



هر چند اين مباحث ساده به نظر مي‌رسند ولي در صورت عدم رعايت سبب سرخ و سفيد شدن در هنگام مقتضي خواهند گرديد.

۱۳۸۹/۰۸/۱۳

نحوه‌ي صحيح فراخواني SQL Aggregate Functions حين استفاده از LINQ


SQL Aggregate Functions كه مد نظر شما هستند مانند Min ، Max ، Sum و امثال آن. بحث LINQ هم زمانيكه از الگوي Repository استفاده شود مستقل از نوع ORM مورد نظر خواهد شد؛ بنابراين در اينجا مقصود از LINQ مي‌تواند LINQ to SQL ، LINQ to Entities ، LINQ to NHibernate و كلا هر نوع ORM ديگري با پشتيباني از LINQ باشد.
صورت مساله هم اين است: آيا نوشتن عبارت LINQ ايي به شكل زير صحيح است؟
decimal amount = respository.Transactions
.Where(t=>t.TransactionDate>new DateTime(2010,10,13))
.Sum(t=>t.Amount);
پاسخ: خير!
توضيحات:
عبارت LINQ فوق در نهايت به شكل زير ترجمه خواهد شد:
-- Region Parameters
-- @p0: DateTime [2010/10/13 12:00:00 ق.ظ]
-- EndRegion
SELECT SUM([t0].[Amount]) AS [value]
FROM [Transactions] AS [t0]
WHERE [t0].[TransactionDate] > @p0
و اتفاقا در اين سيستم پس از تاريخ 2010/10/13 هيچ تراكنشي ثبت نشده است؛ بنابراين خروجي اين كوئري null خواهد بود و نه صفر. همينجا است كه يكي از استثناهاي زير صادر شده و ادامه‌ي برنامه با مشكل مواجه خواهد شد:
- System.InvalidOperationException: The cast to value type 'decimal' failed because the materialized value is null.
- InvalidOperationException: The null value cannot be assigned to a member with type decimal which is a non-nullable value type.

مشكل هم از اينجا ناشي مي‌شود كه متغييري از نوع deciaml يا int و امثال آن، مقدار دريافتي نال را نمي‌پذيرند. براي رفع اين مشكل بايد عبارت LINQ فوق به صورت زير بازنويسي شود (و اهميتي هم ندارد كه Sum است يا Max يا Avg و غيره؛ در مورد بكارگيري تمام SQL Aggregate Functions در يك عبارت LINQ ، اين مورد بايد لحاظ گردد):
decimal amount = respository.Transactions
.Where(t=>t.TransactionDate>new DateTime(2010,10,13))
.Sum(t=>(decimal?)t.Amount)??0;

دقيقا به همين علت است كه در دات نت، nullable types تعريف شده‌اند. امكان ذخيره سازي null‌ در يك متغير براي مثال از نوع decimal وجود ندارد اما نوع decimal? (و يا Nullable<decimal> به بياني ديگر) اين قابليت را دارد.
شايد بگوئيد كه در اينجا با تغيير تعريف متغير به decimal? amount مشكل حل مي‌شود، اما خير. تعريف extension method مربوط به sum به صورت زير است:

public static TResult Sum<TSource>(
this IQueryable<TSource> source,
Expression<Func<TSource, TResult>> selector)

در اين تعريف به TResult دقت نمائيد؛ هم بيانگر نوع خروجي نهايي متد و هم مشخص سازنده‌ي نوع پارامتري است كه خروجي Lambda Expression را تشكيل مي‌دهد. به اين معنا كه سي شارپ، TResult را از lambda expression دريافت كرده و خروجي Sum را بر همان مبنا و نوع تشكيل مي‌دهد. بنابراين براي دريافت خروجي nullable بايد TResult ايي nullable را همانند مثال فوق ايجاد كنيم.

خلاصه بحث:
اگر در كدهاي LINQ خود كه با بانك اطلاعاتي سر و كار دارند از معادل‌هاي SQL Aggregate Functions استفاده كرده‌ايد، آن‌ها را يافته و نكته‌ي nullable TResult فوق را به آن‌ها اعمال كنيد؛ در غير اينصورت منتظر باشيد تا روزي برنامه شما به سادگي كرش كند.


۱۳۸۹/۰۸/۰۷

تفاوت بين IQueryable و IEnumerable در حين كار با ORMs


متد زير را كه يكي از اشتباهات رايج حين استفاده از LINQ خصوصا جهت Binding اطلاعات است، در نظر بگيريد:
IQueryable<Customer> GetCustomers()

اين متد در حقيقت هيچ چيزي را Get نمي‌كند! نام اصلي آن GetQueryableCustomers و يا GetQueryObjectForCustomersاست.
IQueryable قلب LINQ است و تنها بيانگر يك عبارت (expression) از ركوردهايي مي‌باشد كه مد نظر شما است و نه بيشتر.
IQueryable<Customer> youngCustomers = repo.GetCustomers().Where(m => m.Age < 15);
براي مثال زمانيكه يك IQueryable را همانند مثال فوق فيلتر مي‌كنيد نيز هنوز چيزي از بانك اطلاعاتي يا منبع داده‌اي دريافت نشده است. هنوز هيچ اتفاقي رخ نداده است و هنوز رفت و برگشتي به منبع داده‌اي صورت نگرفته است.
به آن بايد به شكل يك expression builder نگاه كرد و نه ليستي از اشياء فيلتر شده‌ي ما. به اين مفهوم، deferred execution (اجراي به تاخير افتاده) نيز گفته مي‌شود (بايد دقت داشت كه IQueryable هم يك نوع IEnumerable است به علاوه expression trees كه مهم‌ترين وجه تمايز آن نيز مي‌باشد).
براي مثال در عبارت زير تنها در زمانيكه متد ToList فراخواني مي‌شود، كل عبارت LINQ ساخته شده، به عبارت SQL متناظر با آن ترجمه شده، اطلاعات از ديتابيس اخذ گرديده و حاصل به صورت يك ليست بازگشت داده مي‌شود:
IList<Competitor> competitorRecords =  competitorRepository
.Competitors
.Where(m => !m.Deleted)
.OrderBy(m => m.countryId)
.ToList(); //فقط اينجا است كه اس كيوال نهايي توليد مي‌شود

در مورد IEnumerable ها چطور؟
IEnumerable<Product> products = repository.GetProducts();
var productsOver25 = products.Where(p => p.Cost >= 25.00);
دو سطر فوق به اين معنا است:
لطفا ابتدا به بانك اطلاعاتي رجوع كن و تمام ركوردهاي محصولات موجود را بازگشت بده. سپس بر روي اين حجم بالاي اطلاعات، محصولاتي را كه قيمت بالاي 25 دارند، فيلتر كن.

اگر همين دو سطر را با IQueryable بازنويسي كنيم چطور؟
 IQueryable<Product> products = repository.GetQueryableProducts();
var productsOver25 = products.Where(p => p.Cost >= 25.00);
در سطر اول تنها يك عبارت LINQ ساخته شده است و بس. در سطر دوم نيز به همين صورت. در طي اين دو سطر حتي يك رفت و برگشت به بانك اطلاعاتي صورت نخواهد گرفت. در ادامه اگر اين اطلاعات به نحوي Select شوند (يا ToList فراخواني شود، يا در طي يك حلقه براي مثال Iteration ايي روي اين حاصل صورت گيرد يا موارد مشابه ديگر)، آنگاه كوئري SQL متناظر با عبارت LINQ فوق ساخته شده و بر روي بانك اطلاعاتي اجرا خواهد شد.
بديهي است اين روش منابع كمتري را نسبت به حالتي كه تمام اطلاعات ابتدا دريافت شده و سپس فيلتر مي‌شوند، مصرف مي‌كند (حالت بازگشت تمام اطلاعات ممكن است شامل 20000 ركورد باشد، اما حالت دوم شايد فقط 5 ركورد را بازگشت دهد).

سؤال: پس IQueryable بسيار عالي است و از اين پس كلا از IEnumerable ها ديگر نبايد استفاده كرد؟
خير! توصيه اكيد طراحان اين است كه لطفا تا حد امكان متدهايي كه IQueryable بازگشت مي‌دهند ايجاد نكنيد! IQueryable يعني اينكه اين نقطه‌ي آغازين كوئري در اختيار شما، بعد برو هر كاري كه دوست داشتي با آن در طي لايه‌هاي مختلف انجام بده و هر زمانيكه دوست داشتي از آن يك خروجي تهيه كن. خروجي IQueryable به معناي مشخص نبودن زمان اجراي نهايي كوئري و همچنين مبهم بودن نحوه‌ي استفاده از آن است. به همين جهت متدهايي را طراحي كنيد كه IEnumerable بازگشت مي‌دهند اما در بدنه‌ي آن‌ها به نحو صحيح و مطلوبي از IQueryable استفاده شده است. به اين صورت حد و مرز يك متد كاملا مشخص مي‌شود. متدي كه واقعا همان فيلتر كردن محصولات را انجام مي‌دهد، همان 5 ركورد را بازگشت خواهد داد؛ اما با استفاده از يك ليست يا يك IEnumerable و نه يك IQueryable كه پس از فراخواني متد نيز به هر نحو دلخواهي قابل تغيير است.

۱۳۸۹/۰۸/۰۵

Count يا Any


با وجود امكانات مهياي توسط LINQ ، يك سري از عادات متداول حين كار با گروهي از اشياء بايد كنار گذاشته شوند؛ براي مثال چگونگي بررسي اين مطلب كه آيا شيء IEnumerable ما حاوي عنصري هست يا خير.
روش متداول انجام اينكار استفاده از متد Count است. چون اين متد پيش از تدارك امكانات LINQ نيز وجود داشته، بنابراين اولين موردي كه جهت بررسي آن به ذهن خطور مي‌كند، استفاده از متد Count مي‌باشد؛ براي مثال:
void Method(IEnumerable<Status> statuses)
{
if (statuses != null && statuses.Count() > 0)
// do something...
}
اين روش بهينه نيست زيرا كار متد Count بررسي تك تك عناصر شيء IEnumerable و سپس بازگرداندن تعداد آن‌ها است. اين مورد خصوصا در حالت‌هاي كار با بانك اطلاعاتي و تنظيمات lazy-loading آن و يا تعداد بالاي عناصر يك ليست، بسيار هزينه‌بر خواهد شد.
ولي در اينجا هدف ما اين است كه آيا شيء IEnumerable داراي حداقل يك عنصر است يا خير؟ بنابراين بجاي استفاده از متد Count بهتر است از يكي از extension methods فراهم شده توسط LINQ به نام Any استفاده شود.
كار متد Any ، پس از بررسي اولين عنصر يك مجموعه، خاتمه خواهد يافت و بديهي است كه نسبت به متد Count بسيار سريعتر و كم هزينه‌تر خواهد بود. علاوه بر آن حين كار با بانك‌هاي اطلاعاتي براي مثال توسط LINQ to Entities ، در SQL نهايي توليدي به EXISTS ترجمه خواهد شد.
void Method(IEnumerable<Status> statuses)
{
if (statuses != null && statuses.Any())
// do something...
}
خلاصه‌ي بحث:
از اين پس حين استفاده از انواع و اقسام ليست‌ها، آرايه‌ها، IEnumerable ها و امثال آن‌ها، جهت بررسي خالي بودن يا نبودن آن‌ها تنها از متد Any فراهم شده توسط LINQ استفاده نمائيد.
if (myArray != null && myArray.Any())
// do something...

۱۳۸۸/۱۰/۰۵

Fluent Linq to Sql


نگارش بعدي يا چهارم entity framework چيزي است شبيه به Fluent NHibernate . يعني اگر مقاله‌اي را در اين زمينه مطالعه كنيد و عنوان آن حذف شود، نمي‌توان تشخيص داد كه اين مقاله مربوط به entity framework است يا Fluent NHibernate. هر چند entity framework حداقل دو نگارش ديگر لازم دارد تا NHibernate را كاملا پشت سر بگذارد.
از آن طرف محبوبيت Linq to SQL هم هنوز پابرجا است و براي مثال سايت پر ترافيكي مثل stack overflow از آن استفاده مي‌كند و بسيار هم موفق بوده و كارش را به خوبي انجام مي‌دهد.
پروژه مكملي به نام Fluent Linq to Sql با الهام گيري از Fluent NHibernate در سايت codeplex موجود است كه اين نوع نگاشت‌ها را براي Linq to Sql نيز ميسر مي‌سازد. به اين صورت ديگر نيازي به استفاده از attributes و يا فايل‌هاي xml نگاشت‌هاي Linq to Sql نخواهد بود. همچنين مدل كاري اول كد بعد ديتابيس نيز به اين صورت محقق مي‌شود.



۱۳۸۸/۰۸/۱۸

پردازش موازي در VS2010


دموي نسبتا مفصلي از توانايي‌هاي دات نت فريم ورك 4 و VS2010 را كه توسط تيم مربوطه در مورد پردازش موازي تهيه شده است، از آدرس زير مي‌توانيد دريافت نمائيد.





۱۳۸۸/۰۸/۱۵

امنيت در LINQ to SQL


سؤال: LINQ to SQL تا چه ميزان در برابر حملات تزريق SQL امن است؟
جواب كوتاه: بسيار زياد!

توضيحات:
string query = @"SELECT * FROM USER_PROFILE
WHERE LOGIN_ID = '"+loginId+@"' AND PASSWORD = '"+password+@"'";
گاهي از اوقات هر چقدر هم در مورد خطرات كوئري‌هايي از نوع فوق مقاله نوشته شود كافي نيست و باز هم شاهد اين نوع جمع زدن‌ها و نوشتن كوئري‌هايي به شدت آسيب پذير در حالت استفاده از ADO.Net كلاسيك هستيم. مثال فوق يك نمونه كلاسيك از نمايش آسيب پذيري در مورد تزريق اس كيوال است. يا نمونه‌ي بسيار متداول ديگري از اين دست كه با ورودي خطرناك مي‌تواند تا نمايش كليه اطلاعات تمامي جداول موجود هم پيش برود:
protected void btnSearch_Click(object sender, EventArgs e)
{
String cmd = @"SELECT [CustomerID], [CompanyName], [ContactName]
FROM [Customers] WHERE CompanyName ='" + txtCompanyName.Text
+ @"'";

SqlDataSource1.SelectCommand = cmd;

GridView1.Visible = true;
}
در اينجا فقط كافي است مهاجم با تزريق عبارت SQL مورد نظر خود، كوئري اوليه را كاملا غيرمعتبر كرده و از يك جدول ديگر در سيستم كوئري تهيه كند!
راه حلي كه براي مقابله با آن در دات نت ارائه شده نوشتن كوئري‌هاي پارامتري است و در اين حالت كار encoding اطلاعات ورودي به صورت خودكار توسط فريم ورك مورد استفاده انجام خواهد شد؛ همچنين براي مثال اس كيوال سرور، execution plan اين نوع كوئري‌هاي پارامتري را همانند رويه‌هاي ذخيره شده، كش كرده و در دفعات آتي فراخواني آن‌ها به شدت سريعتر عمل خواهد كرد. براي مثال:
SqlCommand cmd = new SqlCommand("SELECT UserID FROM Users WHERE UserName=@UserName AND Password=@Password");
cmd.Parameters.Add(new SqlParameter("@UserName", System.Data.SqlDbType.NVarChar, 255, UserName));
cmd.Parameters.Add(new SqlParameter("@Password", System.Data.SqlDbType.NVarChar, 255, Password));
dr = cmd.ExecuteReader();
if (dr.Read()) userId = dr.GetInt32(dr.GetOrdinal("UserID"));
زمانيكه از كوئري پارامتري استفاده شود، مقدار پارامتر، هيچگاه فرصت و قدرت اجرا پيدا نمي‌كند. در اين حالت صرفا به آن به عنوان يك مقدار معمولي نگاه خواهد شد و نه جزء قابل تغيير بدنه كوئري وارد شده كه در حالت جمع زدن رشته‌ها همانند اولين كوئري معرفي شده، تا حد انحراف كوئري به يك كوئري دلخواه مهاجم قابل تغيير است.

اما در مورد LINQ to SQL چطور؟
اين سيستم به صورت پيش فرض طوري طراحي شده است كه تمام كوئري‌هاي SQL نهايي حاصل از كوئري‌هاي LINQ نوشته شده توسط آن، پارامتري هستند. به عبارت ديگر اين سيستم به صورت پيش فرض براي افرادي كه داراي حداقل اطلاعات امنيتي هستند به شدت امنيت بالايي را به همراه خواهد آورد.
براي مثال كوئري LINQ زير را در نظر بگيريد:
var products = from p in db.products
where p.description.StartsWith(_txtSearch.Text)
select new
{
p.description,
p.price,
p.stock

};
اكنون فرض كنيد كاربر به دنبال كلمه sony باشد، آنچه كه بر روي اس كيوال سرور اجرا خواهد شد، دستور زير است (ترجمه نهايي كوئري فوق به زبان T-SQL) :
exec sp_executesql N'SELECT [t0].[description], [t0].[price], [t0].[stock]
FROM [dbo].[products] AS [t0]
WHERE [t0].[description] LIKE @p0',N'@p0 varchar(5)',@p0='sony%'
براي لاگ كردن اين عبارات SQL يا مي‌توان از SQL profiler استفاده نمود و يا خاصيت log زمينه مورد استفاده را بايد مقدار دهي كرد:
 db.Log = Console.Out;
و يا مي‌توان بر روي كوئري مورد نظر در VS.Net يك break point قرار داد و سپس از debug visualizer مخصوص آن استفاده نمود.

همانطور كه ملاحظه مي‌كنيد، كوئري نهايي توليد شده پارامتري است و در صورت ورود اطلاعات خطرناك در پارامتر p0 ، هيچ اتفاق خاصي نخواهد افتاد و صرفا ركوردي بازگشت داده نمي‌شود.

و يا همان مثال كلاسيك اعتبار سنجي كاربر را در نظر بگيريد:
public bool Validate(string loginId, string password)
{
DataClassesDataContext db = new DataClassesDataContext();

var validUsers = from user in db.USER_PROFILEs
where user.LOGIN_ID == loginId
&& user.PASSWORD == password
select user;

if (validUsers.Count() > 0) return true;
else return false;
}
كوئري نهايي T-SQL توليد شده توسط اين ORM از كوئري LINQ فوق به شكل زير است:
SELECT [t0].[LOGIN_ID], [t0].[PASSWORD]
FROM [dbo].[USER_PROFILE] AS [t0]
WHERE ([t0].[LOGIN_ID] = @p0) AND ([t0].[PASSWORD] = @p1)
و اين كوئري پارامتري نيز در برابر حملات تزريق اس كيوال امن است.

تذكر مهم هنگام استفاده از سيستم LINQ to SQL :

اگر با استفاده از LINQ to SQL مجددا به روش قديمي اجراي مستقيم كوئري‌هاي SQL خود همانند مثال زير روي بياوريد (اين امكان نيز وجود دارد)، نتيجه اين نوع كوئري‌هاي حاصل از جمع زدن رشته‌ها، پارامتري "نبوده" و مستعد به تزريق اس كيوال هستند:
string sql = "select * from Trade where DealMember='" + this.txtParams.Text + "'";
var trades = driveHax.ExecuteQuery<Trade>(sql);
در اينجا بايد در نظر داشت كه اگر شخصي مجددا بخواهد از اين نوع روش‌هاي كلاسيك استفاده كند شايد همان ADO.Net كلاسيك براي او كافي باشد و نيازي به تحميل سربار يك ORM را به سيستم نداشته باشد. در اين حالت برنامه از type safety كوئري‌هاي LINQ نيز محروم شده و يك لايه بررسي مقادير و پارامترها را توسط كامپايلر نيز از دست خواهد داد.

اما روش صحيحي نيز در مورد بكارگيري متد ExecuteQuery وجود دارد. استفاده از اين متد به شكل زير مشكل را حل خواهد كرد:
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
"SELECT contactname FROM customers WHERE city = {0}", "Tehran");
در اين حالت، پارامترهاي بكارگرفته شده (همان {0} ذكر شده در كوئري) به صورت خودكار به پارامترهاي T-SQL ترجمه خواهند شد و مشكل تزريق اس كيوال برطرف خواهد شد (به عبارت ديگر استفاده از +، علامت مستعد بودن به تزريق اس كيوال است و بر عكس).

Vote on iDevCenter

۱۳۸۸/۰۸/۰۶

پيدا كردن آيتم‌هاي تكراري در يك ليست به كمك LINQ


گاهي از اوقات نياز مي‌شود تا در يك ليست، آيتم‌هاي تكراري موجود را مشخص كرد. به صورت پيش فرض متد Distinct براي حذف مقادير تكراري در يك ليست با استفاده از LINQ موجود است كه البته آن‌هم اما و اگرهايي دارد كه در ادامه به آن پرداخته خواهد شد، اما باز هم اين مورد پاسخ سؤال اصلي نيست (نمي‌خواهيم موارد تكراري را حذف كنيم).

براي حذف آيتم‌هاي تكراري از يك ليست جنريك مي‌توان متد زير را نوشت:
public static List<T> RemoveDuplicates<T>(List<T> items)
{
return (from s in items select s).Distinct().ToList();
}
براي مثال:
public static void TestRemoveDuplicates()
{
List<string> sampleList =
new List<string>() { "A1", "A2", "A3", "A1", "A2", "A3" };
sampleList = RemoveDuplicates(sampleList);
foreach (var item in sampleList)
Console.WriteLine(item);
}
اين متد بر روي ليست‌هايي با نوع‌هاي اوليه مانند string‌ و int و امثال آن درست كار مي‌كند. اما اكنون مثال زير را در نظر بگيريد:
public class Employee
{
public int ID { get; set; }
public string FName { get; set; }
public int Age { get; set; }
}

public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates<Employee>(lstEmp);

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
اگر متد TestRemoveDuplicates را اجرا نمائيد، ركورد تكراري اين ليست جنريك حذف نخواهد شد؛ زيرا متد distinct بكارگرفته شده نمي‌داند اشيايي از نوع كلاس سفارشي Employee را چگونه بايد با هم مقايسه نمايد تا بتواند موارد تكراري آن‌ها را حذف كند.
براي رفع اين مشكل بايد از آرگومان دوم متد distinct جهت معرفي وهله‌اي از كلاسي كه اينترفيس IEqualityComparer را پياده سازي مي‌كند، كمك گرفت.
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
كه نمونه‌اي از پياده سازي آن به شرح زير مي‌تواند باشد:

public class EmployeeComparer : IEqualityComparer<Employee>
{
public bool Equals(Employee x, Employee y)
{
//آيا دقيقا يك وهله هستند؟
if (Object.ReferenceEquals(x, y)) return true;

//آيا يكي از وهله‌ها نال است؟
if (Object.ReferenceEquals(x, null) ||
Object.ReferenceEquals(y, null))
return false;

return x.Age == y.Age && x.FName == y.FName && x.ID == y.ID;
}

public int GetHashCode(Employee obj)
{
if (Object.ReferenceEquals(obj, null)) return 0;
int hashTextual = obj.FName == null ? 0 : obj.FName.GetHashCode();
int hashDigital = obj.Age.GetHashCode();
return hashTextual ^ hashDigital;
}
}
اكنون اگر يك overload براي متد RemoveDuplicates با درنظر گرفتن IEqualityComparerتهيه كنيم، به شكل زير خواهد بود:
public static List<T> RemoveDuplicates<T>(List<T> items, IEqualityComparer<T> comparer)
{
return (from s in items select s).Distinct(comparer).ToList();
}
به اين صورت متد آزمايشي ما به شكل زير (كه وهله‌اي از كلاس EmployeeComparer‌ به آن ارسال شده) تغيير خواهد كرد:
public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates(lstEmp, new EmployeeComparer());

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
پس از اين تغيير، حاصل اين متد تنها دو ركورد غيرتكراري مي‌باشد.

سؤال: براي يافتن آيتم‌هاي تكراري يك ليست چه بايد كرد؟
احتمالا مقاله "روش‌هايي براي حذف ركوردهاي تكراري" را به خاطر داريد. اينجا هم مي‌توان كوئري LINQ ايي را نوشت كه ركوردها را بر اساس سن، گروه بندي كرده و سپس گروه‌هايي را كه بيش از يك ركورد دارند، انتخاب نمايد.
public static void FindDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

var query = from c in lstEmp
group c by c.Age into g
where g.Count() > 1
select new { Age = g.Key, Count = g.Count() };

foreach (var item in query)
{
Console.WriteLine("Age {0} has {1} records", item.Age, item.Count);
}
}


Vote on iDevCenter

۱۳۸۸/۰۸/۰۴

استفاده از LINQ جهت تهيه كدهايي كوتاه‌تر و خواناتر


با كمك امكانات ارائه شده توسط LINQ ، مي‌توان بسياري از اعمال برنامه نويسي را در حجمي كمتر، خواناتر و در نتيجه با قابليت نگهداري بهتر، انجام داد كه تعدادي از آن‌ها را در ادامه مرور خواهيم كرد.

الف) تهيه يك يك رشته، حاوي عناصر يك آرايه، جدا شده با كاما.

using System.Linq;

public class CLinq
{
public static string GetCommaSeparatedListNormal(string[] data)
{
string items = string.Empty;

foreach (var item in data)
{
items += item + ", ";
}

return items.Remove(items.Length - 2, 1).Trim();
}

public static string GetCommaSeparatedList(string[] data)
{
return data.Aggregate((s1, s2) => s1 + ", " + s2);
}
}
همانطور كه ملاحظه مي‌كنيد در روش دوم با استفاده از LINQ Aggregate extension method ، كد جمع و جورتر و خواناتري نسبت به روش اول حاصل شده است.

ب) پيدا كردن تعداد عناصر يك آرايه حاوي مقداري مشخص
براي مثال آرايه زير را در نظر بگيريد:

var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
قصد داريم تعداد عناصر حاوي name را مشخص سازيم.
در تابع GetCountNormal زير، اين كار به شكلي متداول انجام شده و در GetCount از LINQ Count extension method كمك گرفته شده است.

using System.Linq;

public class CLinq
{
public static int GetCountNormal()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
var count = 0;
foreach (var name in names)
{
if (name.Contains("name"))
count += 1;
}
return count;
}

public static int GetCount()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
return names.Count(name => name.Contains("name"));
}
}
به نظر شما كدام روش خواناتر بوده و نگهداري و يا تغيير آن در آينده ساده‌تر مي‌باشد؟

ج) دريافت ليستي از عناصر شروع شده با يك عبارت
در اينجا نيز دو روش متداول و استفاده از LINQ بررسي شده است.

using System.Linq;
using System.Collections.Generic;

public class CLinq
{
public static List<string> GetListNormal()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
List<string> result = new List<string>();
foreach (var item in sampleList)
{
if (item.StartsWith("P"))
result.Add(item);
}
return result;
}

public static List<string> GetList()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
return sampleList.Where(x => x.StartsWith("P")).ToList();
}
}

و در حالت كلي، اكثر حلقه‌هاي foreach متداول را مي‌توان با نمونه‌هاي خواناتر كوئري‌هاي LINQ معادل، جايگزين كرد.

Vote on iDevCenter

۱۳۸۸/۰۶/۰۸

استثناي Sequence contains no elements در حين استفاده از LINQ


در ابتدا مثال‌هاي زير را در نظر بگيريد:

using System;
using System.Collections.Generic;
using System.Linq;

namespace testWinForms87
{
public class Data
{
public int id { get; set; }
public string name { get; set; }
}

class CLinqTests
{
public static int TestGetListMin1()
{
var lst = new List<Data>
{
new Data{ id=1, name="id1"},
new Data{ id=2, name="id2"},
new Data{ id=3, name="name3"}
};

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}

public static int TestGetListMin2()
{
var lst = new List<Data>();

return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
}
}
در متد TestGetListMin1 قصد داريم كوچكترين آي دي ركوردهايي را كه نام آن‌ها حاوي id است، از ليست تشكيل شده از كلاس Data بدست آوريم (همانطور كه مشخص است سه ركورد از نوع Data در ليست lst ما قرار گرفته‌اند).
محاسبات آن كار مي‌كند و مشكلي هم ندارد. اما هميشه در دنياي واقعي همه چيز قرار نيست به اين خوبي پيش برود. ممكن است همانند متد TestGetListMin2 ، ليست ما خالي باشد (براي مثال از ديتابيس، ركوردي مطابق شرايط كوئري‌هاي قبلي بازگشت داده نشده باشد). در اين حالت هنگام فراخواني متد Min ، استثناي Sequence contains no elements رخ خواهد داد و همانطور كه در مباحث defensive programming عنوان شد، وظيفه‌ي ما اين نيست كه خودرو را به ديوار كوبيده (يا منتظر شويم تا كوبيده شود) و سپس به فكر چاره بيفتيم كه خوب، عجب! مشكلي رخ داده است!
اكنون چه بايد كرد؟ حداقل يك مرحله بررسي اينكه آيا كوئري ما حاوي ركوردي مي‌باشد يا خير بايد به اين متد اضافه شود (به صورت زير):

public static int TestGetListMin3()
{
var lst = new List<Data>();
var query = from c in lst
where c.name.Contains("id")
select c.id;

if (query.Any())
return query.Min();
else
return -1;
}
البته مي‌شد اگر هيچ ركوردي بازگشت داده نمي‌شد، يك استثناي سفارشي را ايجاد كرد، اما به شخصه ترجيح مي‌دهم عدد منهاي يك را بر گردانم (چون مي‌دانم ركوردهاي من عدد مثبت هستند و اگر حاصل منفي شد نيازي به ادامه‌ي پروسه نيست).

شبيه به اين مورد در هنگام استفاده از تابع Single مربوط به LINQ نيز ممكن است رخ دهد (توليد استثناي ذكر شده) اما در اينجا مايكروسافت تابع SingleOrDefault را نيز پيش بيني كرده است. در اين حالت اگر كوئري ما ركوردي را برنگرداند، SingleOrDefault مقدار نال را برگشت داده و استثنايي رخ نخواهد داد (نمونه‌ي ديگر آن متدهاي First و FirstOrDefault هستند).
در مورد متدهاي Min و Max ، متدهاي MinOrDefault يا MaxOrDefault در دات نت فريم ورك وجود ندارند. مي‌توان اين نقيصه را با استفاده از extension methods برطرف كرد.

using System;
using System.Collections.Generic;
using System.Linq;

public static class LinqExtensions
{
public static T MinOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Min<T>();

return defaultValue;
}

public static T MaxOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Max<T>();

return defaultValue;
}
}
اكنون با استفاده از extension methods فوق، كد ما به صورت زير تغيير خواهد كرد:

public static int TestGetListMin4()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).MinOrDefault(-1);
}

۱۳۸۸/۰۵/۳۰

استفاده از LINQ جهت جستجوي فايل‌ها


يكي ديگر از كاربردهاي anonymous types ، امكان استفاده از قابليت‌هاي LINQ براي جستجوي فايل‌ها و پوشه‌ها است.
مثال:

using System;
using System.Linq;
using System.IO;

namespace LINQtoDir
{
class Program
{
static void Main(string[] args)
{
var query = from f in new DirectoryInfo(@"C:\Documents and Settings\vahid\My Documents\My Pictures")
.GetFiles("*.*", SearchOption.AllDirectories)
where f.Extension.ToLower() == ".png" || f.Extension.ToLower() == ".jpg"
orderby f.LastAccessTime
select new
{
DateLastModified = f.LastWriteTime,
Extension = f.Extension,
Size = f.Length,
FileName = f.Name
};

foreach (var file in query)
Console.WriteLine(file.FileName);

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

در اين مثال توسط كوئري نوشته شده، تمامي تصاوير jpg و يا png موجود در پوشه my pictures يافت شده و سپس بر اساس LastAccessTime مرتب مي‌شوند. در آخر با استفاده از anonymous types ، يك شيء IEnumerable از خواص مورد نظر فايل‌هاي يافت شده، بازگشت داده مي‌شود. اكنون هر استفاده‌ي دلخواهي را مي‌توان از اين شيء انجام داد.

۱۳۸۸/۰۵/۲۳

استفاده از LINQ to XML جهت خواندن فيدهاي RSS


مثال زير را به عنواني نمونه‌اي از كاربرد LINQ to XML براي خواندن فيدهاي RSS كه اساسا به فرمت XML هستند مي‌توان ارائه داد.
ابتدا كد كامل مثال را در نظر بگيريد:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace LinqToRSS
{
public static class LanguageExtender
{
public static string SafeValue(this XElement input)
{
return (input == null) ? string.Empty : input.Value;
}

public static DateTime SafeDateValue(this XElement input)
{
return (input == null) ? DateTime.MinValue : DateTime.Parse(input.Value);
}
}

public class RssEntry
{
public string Title { set; get; }
public string Description { set; get; }
public string Link { set; get; }
public DateTime PublicationDate { set; get; }
public string Author { set; get; }
public string BlogName { set; get; }
public string BlogAddress { set; get; }
}

public class Rss
{
static XElement selectDate(XElement date1, XElement date2)
{
return date1 ?? date2;
}

public static List<RssEntry> GetEntries(string feedUrl)
{
//applying namespace in an XElement
XName xn = XName.Get("{http://purl.org/dc/elements/1.1/}creator");//{namespace}root
XName xn2 = XName.Get("{http://purl.org/dc/elements/1.1/}date");

var feed = XDocument.Load(feedUrl);
if (feed.Root == null) return null;

var items = feed.Root.Element("channel").Elements("item");
var feedQuery =
from item in items
select new RssEntry
{
Title = item.Element("title").SafeValue(),
Description = item.Element("description").SafeValue(),
Link = item.Element("link").SafeValue(),
PublicationDate =
selectDate(item.Element(xn2), item.Element("pubDate")).SafeDateValue(),
Author = item.Element(xn).SafeValue(),
BlogName = item.Parent.Element("title").SafeValue(),
BlogAddress = item.Parent.Element("link").SafeValue()
};

return feedQuery.ToList();
}
}

class Program
{
static void Main(string[] args)
{
List<RssEntry> entries = Rss.GetEntries("http://weblogs.asp.net/aspnet-team/rss.aspx");
if (entries != null)
foreach (var item in entries)
Console.WriteLine(item.Title);

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

توضيحات:
1- در اين مثال فقط جهت سهولت بيان آن در يك صفحه، تمامي كلاس‌هاي تعريف شده در يك فايل آورده شدند. اين روش صحيح نيست و بايد به ازاي هر كلاس يك فايل جدا در نظر گرفته شود.
2- كلاس LanguageExtender از قابليت extension methods سي شارپ 3 استفاده مي‌كند. به اين صورت كلاس XElement دات نت بسط يافته و دو متد به آن اضافه مي‌شود كه به سادگي در كدهاي خود مي‌توان از آن‌ها استفاده كرد. هدف آن هم بررسي نال بودن يك آيتم دريافتي و ارائه‌ي حاصلي امن براي اين مورد است.
3- كلاس RssEntry به جهت استفاده در خروجي كوئري LINQ تعريف شد. مي‌خواهيم خروجي نهايي، يك ليست جنريك از نوع RssEntry باشد.
4- متد اصلي برنامه، GetEntries است. اين متد آدرس اينترنتي يك فيد را دريافت كرده و پس از آناليز، آن‌را به صورت يك ليست بر مي‌گرداند.

<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://weblogs.asp.net/utility/FeedStylesheets/rss.xsl" media="screen"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>Latest Microsoft Blogs</title>
<link>http://weblogs.asp.net/aspnet-team/default.aspx</link>
<description />
<dc:language>en</dc:language>
<generator>CommunityServer 2007 SP1 (Build: 20510.895)</generator>
<item>
<title>Comments on my recent benchmarks.</title>
<link>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/</link>
<pubDate>Mon, 10 Aug 2009 23:33:59 GMT</pubDate>
<guid isPermaLink="false">c06e2b9d-981a-45b4-a55f-ab0d8bbfdc1c:7166225</guid>
<dc:creator>Misfit Geek: msft</dc:creator>
<slash:comments>0</slash:comments>
<wfw:commentRss xmlns:wfw="http://wellformedweb.org/CommentAPI/">http://weblogs.asp.net/aspnet-team/rsscomments.aspx?PostID=7166225</wfw:commentRss>
<comments>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/#comments</comments>
<description>Overall I’ve been pretty impressed ...</description>
<category domain="http://weblogs.asp.net/aspnet-team/archive/tags/ASP.NET/default.aspx">ASP.NET</category>
</item>
</channel>
</rss>
براي نمونه خروجي يك فيد مي‌تواند به صورت فوق باشد. آيتم‌هاي آن به صورت قابل بيان است:
var items = feed.Root.Element("channel").Elements("item");
و نكته مهمي كه اينجا وجود دارد، اعمال فضاهاي نام بكار رفته در اين فايل xml پيشرفته مي‌باشد. براي اعمال فضاهاي نام به يكي از دو روش زير مي‌توان عمل كرد:

XName.Get("{mynamespace}root");
//or
XName.Get("root", "mynamespace");

۱۳۸۸/۰۵/۲۱

خلاصه‌اي از LINQ to XML


در اين مقاله مروري سريع و كاربردي خواهيم داشت بر توانايي‌هاي مقدماتي LINQ to XML .

فايل Employee.XML را با محتويات زير در نظر بگيريد:

<Employees>
<Employee>
<Name>Vahid</Name>
<Phone>11111111</Phone>
<Department>IT</Department>
<Age>52</Age>
</Employee>
<Employee>
<Name>Farid</Name>
<Phone>124578963</Phone>
<Department>Civil</Department>
<Age>35</Age>
</Employee>
<Employee>
<Name>Mehdi</Name>
<Phone>1245788754</Phone>
<Department>HR</Department>
<Age>30</Age>
</Employee>
</Employees>

1- چگونه يك فايل XML را جهت استفاده توسط LINQ بارگذاري كنيم؟

قبل از شروع، اسمبلي System.Xml.Linq بايد به ارجاعات برنامه اضافه شود. سپس:

using System.Xml.Linq;

XDocument xDoc = XDocument.Load("Employee.xml");

2- اگر محتويات XML دريافتي به صورت رشته بود (مثلا از يك ديتابيس دريافت شد)، اكنون چگونه بايد آن‌را بارگذاري كرد؟

اين‌كار را با استفاده از يك StringReader به صورت زير مي‌توان انجام داد:

// loading XML from string
StringReader sr = new StringReader(stringXML);
XDocument xDoc = XDocument.Load(sr);

3- چگونه يك كوئري ساده شامل تمامي ركوردهاي Employee مجموعه Employees را تهيه كنيم؟

using System.Collections;

IEnumerable<XElement> empList = from e in xDoc.Root.Elements("Employee") select e;
توسط كوئري فوق، تمامي ركوردهاي كاركنان در يك Collection در اختيار ما خواهند بود. نكته‌ي مهم عبارت LINQ فوق، xDoc.Root.Elements("Employee") مي‌باشد. به اين صورت از xDoc بارگذاري شده، ابتدا Root و يا همان محتواي فايل XML را جهت بررسي انتخاب كرده و سپس گره‌هاي مرتبط با كاركنان را انتخاب مي‌كنيم.
اكنون كه مجموعه كاركنان توسط متغير empList در اختيار ما است، دسترسي به محتويات آن به سادگي زير خواهد بود:

foreach (XElement employee in empList)
{
foreach (XElement e in employee.Elements())
{
Console.WriteLine(e.Name + " = " + e.Value);
}
}
در اين‌جا حلقه خارجي اطلاعات كلي تمامي كاركنان را باز مي‌گرداند و حلقه داخلي اطلاعات يك گره دريافت شده را نمايش مي‌دهد.

4- كوئري بنويسيد كه اطلاعات تمامي كاركنان بخش HR را باز گرداند.

IEnumerable<XElement> hrList = from e in xDoc.Root.Elements("Employee")
where e.Element("Department").Value == "HR"
select e;

همانطور كه ملاحظه مي‌كنيد همانند عبارات SQL ، در تمامي عناصر متعلق به كاركنان، عناصري كه دپارتمان آن‌ها مساوي HR است بازگشت داده مي‌شود.

5- كوئري بنويسيد كه ليست تمامي كاركنان بالاي 30 سال را ارائه دهد.

IEnumerable<XElement> tList = from e in xDoc.Root.Elements("Employee")
where int.Parse(e.Element("Age").Value) > 30
select e;

چون حاصل e.Element("Age").Value يك رشته است، براي اعمال فيلترهاي عددي بايد اين رشته‌ها تبديل به عدد شوند. به همين جهت از int.Parse استفاده شده است.

6- كوئري بنويسيد كه ليست تمامي كاركنان بالاي 30 سال را مرتب شده بر اساس نام باز گرداند.

IEnumerable<XElement> tList = from e in xDoc.Root.Elements("Employee")
where int.Parse(e.Element("Age").Value) > 30
orderby e.Element("Name").Value
select e;
در اينجا همانند عبارات SQL از orderby جهت مرتب سازي بر اساس عناصر نام استفاده شده است.

7- تبديل نتيجه‌ي يك كوئري LINQ به ليستي از اشياء

مفهومي به سي شارپ 3 اضافه شده است به نام anonymous types . براي مثال:



توسط اين قابليت مي‌توان يك شيء را بدون نياز به تعريف ابتدايي آن ايجاد كرد و حتي از intelliSense موجود در IDE نيز بهره مند شد. اين نوع‌هاي ناشناس توسط واژه‌هاي كليدي new و var توليد مي‌شوند. كامپايلر به صورت خودكار براي هر anonymous type يك كلاس ايجاد مي‌كند.
دقيقا از همين توانايي در LINQ نيز مي‌توان استفاده نمود:

var empList = from e in xDoc.Root.Elements("Employee")
orderby e.Element("Name").Value
select new
{
Name = e.Element("Name").Value,
Phone = e.Element("Phone").Value,
Department = e.Element("Department").Value,
Age = int.Parse(e.Element("Age").Value)
};
در اين‌جا حاصل كوئري، تبديل به ليستي از اشياءanonymous مي‌شود. اكنون براي نمايش آن‌ها نيز مي‌توان از واژه كليدي var استفاده نمود كه از هر لحاظ نسبت به روش اعمال foreach بر روي Xelement ها كه در مثال 3 مشاهده كرديم خواناتر است:

foreach (var employee in empList)
{
Console.WriteLine("Name = " + employee.Name);
Console.WriteLine("Dep = " + employee.Department);
Console.WriteLine("Phone = " + employee.Phone);
Console.WriteLine("Age = " + employee.Age);
}
و البته بديهي است كه مي‌توان از anonymous types استفاده نكرد و دقيقا تعريف شيء را پيش از انتخاب آن نيز مشخص نمود. براي مثال:

public class Employee
{
public string Name { get; set; }
public string Phone { get; set; }
public string Department { get; set; }
public int Age { get; set; }
}
در اين حالت، قسمت select new عبارت LINQ ما به select new Employee تغيير خواهد كرد.
براي مثال اگر بخواهيم ليست دريافتي را به صورت يك ليست جنريك بازگشت دهيم خواهيم داشت:

public class Employee
{
public string Name { get; set; }
public string Phone { get; set; }
public string Department { get; set; }
public int Age { get; set; }
}

List<Employee> Get()
{
XDocument xDoc = XDocument.Load("Employee.xml");
var items =
from e in xDoc.Root.Elements("Employee")
orderby e.Element("Name").Value
select new Employee
{
Name = e.Element("Name").Value,
Phone = e.Element("Phone").Value,
Department = e.Element("Department").Value,
Age = int.Parse(e.Element("Age").Value)
};
return items.ToList();
}

۱۳۸۸/۰۳/۱۷

برنامه LINQPad و مثال‌هاي جديد آن


برنامه معروف LINQPad تا كنون به همراه مثال‌هاي كتاب C# 3.0 in a Nutshell به صورت يكپارچه ارائه مي‌شد.
اكنون مثال‌هاي كتاب LINQ in Action نيز قابليت يكپارچگي با اين برنامه را يافته‌اند. به اين صورت بسيار ساده و در همان محيط LINQPad مي‌توان اين مثال‌ها را مرور و اجرا كرد كه در يادگيري LINQ كمك شاياني مي‌نمايند.
براي نصب اين مثال‌هاي يكپارچه جديد، بر روي لينك Download more samples آن كليك كرده و در صفحه‌ي باز شده، بر روي لينكي به نام Download full code listings into LINQPad كليك كنيد.



اكنون مثال‌هاي سي شارپ و VB.Net آن به صورت يكپارچه در اختيار شما خواهند بود.


۱۳۸۷/۱۲/۱۳

ويديوي رايگان LINQ Programming with C# 3.0


مواردي را كه در اين ويديو مشاهده خواهيد كرد:

  • Introduction to LINQ
  • C# 3.0 Language Features
  • LINQ to Objects
  • Lambda Expressions
  • LINQ to DataSets
  • Getting Started with LINQ to SQL
  • Additional LINQ to SQL Features
  • LINQ to XML
  • LINQ to Entities and the Entity Framework

دريافت فايل

ماخذ