Warstwa prezentacji – wprowadzenie

Warstwa prezentacji odpowiedzialna jest za komunikację z użytkownikiem. W dzisiejszych czasach interfejsy graficzne są na tyle rozbudowane, że poprawne zaprojektowanie warstwy prezentacji stanowi poważne wyzwanie. W małych projektach często ta warstwa stanowi najbardziej złożoną część całej architektury.

Bez wykorzystania stosownych wzorców projektowych po pewnym czasie pisania aplikacji okaże się, że jakakolwiek zmiana interfejsu wiąże się ze skomplikowaną refaktoryzacją kodu.

Jedną z podstawowych cech poprawnie zaprojektowanej warstwy prezentacji jest niezależność od sposobu wyświetlania. Doskonałym przykładem jest np. blog WordPress. Za pomocą kilku kliknięć można zmienić wygląd (skórkę) strony. Sposób wyświetlania jest wyraźnie oddzielony od logiki oraz akcji użytkownika. Innymi słowy interfejs graficzny powinien pozostać nieświadomy danych, które wyświetla. Drugim przykładem obok blog’a jest wyświetlanie danych (np. zamówień) w ASP .NET – programista może użyć zarówno kontrolki DataGrid jak Repeater. Podsumowując warstwa prezentacji musi być odporna na zmianę widoku – model musi pozostać niezależny (np. powinno wykorzystywać sie IEnumarable<Order> zamiast specyficznej kolekcji dla danego API graficznego).

Często podawaną ważną cechą warstwy prezentacji jest niezależność od technologii interfejsu graficznego (WPF, Silverlight itp.). W praktyce jednak nie zawsze spełniana jest ta zasada. W idealnym świecie architektura tej warstwy powinna pozwalać na swobodną zmianę technologii UI. W rzeczywistości zaprojektowanie tak elastycznej warstwy jest bardzo trudnym i czasochłonnym zadaniem i często rezygnuje się z tego. Oczywiście wykorzystując np. Model-View-ViewModel znacznie łatwiej będzie przejść w przyszłości na inną technologie niż wykorzystując klasyczny autonomiczny widok.

Ważnym elementem wyróżniającym dobrze zaprojektowaną warstwę jest wsparcie dla testów (np. jednostkowych). W widoku autonomicznym zautomatyzowane testy są praktycznie niemożliwe – sprowadzałyby się do symulowania kliknięć użytkownika. Potrzebujemy API, które pozwoli nam symulować konkretną akcję użytkownika z poziomu kodu – np. kliknięcie w dany przycisk. Kluczem do tego jest stworzenie abstrakcji między widokiem a logiką prezentacji – konkretnie interfejsu zawierającego metody typu “SelectOrder”, “ShowDetails” itp.

Dobrą cechą jest również całkowita niezależność od warstwy biznesowej. Uzyskuje się to poprzez wprowadzenie wspomnianych już w poprzednich postach obiektów DTO. W praktyce jednak tworzenie DTO jest bardzo czasochłonne i nie zawsze opłacalne.

W przyszłych postach przedstawię kilka podstawowych wzorców projektowych – Model-View-Controller, Model-View-Presenter, Model-View-ViewModel oraz Model2. Planuje również opisanie dwóch frameworków wspomagających wykorzystanie tych wzorców zarówno w aplikacjach web (oczywiście będzie to ASP .NET MVC) jak i desktop.

Zasady S.O.L.I.D – Dependency inversion principle

Zaczynamy standardowo od czystej definicji zasady:

  1. Kod z warstw z wyższego poziomu nie powinien zależeć od kodu z niższych warstw. Obie warstwy za to powinny być zależne od abstrakcji.
  2. Abstrakcje nie powinny zależeć od szczegółów (konkretnej implementacji). Z kolei szczegóły (implementacja) powinna zależeć od abstrakcji.

Najlepiej rozważmy to na przykładzie aplikacji enterprise. Kodem z niższej warstwy może być np. DAL (warstwa dostępu do danych) a  z wyższej – warstwa biznesowa. Według zasady, warstwa biznesowa nie może zawierać bezpośrednio referencji do DAL:

class BusinessLayer
{
    private DataContext m_DataContext=new DataContext();
    
    public void SubmitOrder()
    {
        m_DataContext.Orders.Add( new Order());
    }
}

Wyobraźmy sobie, że pewnego dnia zmienia się dostawca warstwy dostępowej (np. z nHibernate na EntityFramework). Wtedy warstwa wyższego poziomu (biznesowa) musi również zostać zaktualizowana. Ponadto ze względu na zmianę kodu ponownie należy przetestować klasę. Zgodnie z zasadą kod powinien być zależny wyłącznie od abstrakcji a nie szczegółów:

class BusinessLayer
{
    private IDataContext m_DataContext=null;

    public BusinessLayer(IDataContext context)
    {
        m_DataContext=context;
    }
    public void SubmitOrder()
    {
        m_DataContext.Orders.Add( new Order());
    }
}

Ewentualna zmiana DAL wymaga tylko wstrzyknięcia poprzez konstruktor konkretnej implementacji. Innym rozwiązaniem jest skorzystanie z wzorca fabryka, plugin lub lokalizator.

Zasady S.O.L.I.D – Interface Segregation Principle

Zasada mówi żeby tworzone przez programistę interfejsy były odpowiedzialne za jak najmniejsza funkcjonalność. Użytkownik chcąc zaimplementować taki interfejs nie powinien pisać metod, których nie potrzebuje. Jeśli znajdują się w nim niepotrzebne metody to wtedy nazywamy go interfejsem “fat” lub “polluted”.

Najlepiej rozważyć to na klasycznym przykładzie (z oodesign):

interface IWorker 
{    
    void Work();
    void Eat();
}
class Worker: IWorker
{    
    public void Work() 
    {        
    }    
    public void Eat() 
    {
    }
}
class Robot: IWorker
{    
    public void Work()
    {    
    }    
    public void Eat()
    {        
      throw new NotImplementedException();
    }
}

Mamy interfejs IWorker z dwoma metodami: Work oraz Eat. Następnie definiujemy dwie konkretne implementacje pracownika. Pierwszy reprezentuje zwykłego pracownika, który musi jeść i pracować (implementacje obydwu metod). Z kolei druga klasa reprezentuje robota, który może tylko pracować – bez jedzenia :). Przykład prezentuje łamanie zasady ISP ponieważ implementacja wymaga dostarczenia dwóch metod a przecież istnieją klasy (Robot) w których wystarczy zaimplementować wyłącznie jedną (Work). Programista nie powinien być zmuszany do pisania pustych metod (dummy methods) tylko po to aby sprostać wymaganiom interfejsu. Prawidłowe rozwiązanie polega na rozłupaniu interfejsu na dwie części:

interface IWorkable 
{    
    public void Work();
}
interface IFeedable
{
    public void Eat();
}
class Worker: IWorkable, IFeedable
{
    public void Work()
    {        
    }        
    public void Eat() 
    {        
    }
}
class Robot: IWorkable
{    
    public void Work() 
    {    
    }
}

Zasady S.O.L.I.D – zasada podstawienia Liskov

Na początek podam czystą definicje z wiki:

“Funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.”

Początkowo za wiele ta tajemnicza definicja nie mówiła mi. Innymi słowy, klasa dziedzicząca powinna  rozszerzać możliwości klasy bazowej a nie całkowicie zmieniać jej funkcjonalność. Sposób korzystania z klasy potomnej powinien być analogiczny do wywoływania klasy bazowej. Przyjrzyjmy się klasycznemu przykładowi łamania zasady Liskov (źródło: http://www.oodesign.com):

class Rectangle
{
    protected int m_Width;    
    protected int m_Height;
    
    public void SetWidth(int width)
    {    
        m_Width = width;    
    }
    public void SetHeight(int height)
    {    
        m_Height = height;    
    }    
    public int GetWidth()
    {        
        return m_Width;    
    }
    public int GetHeight()
    {        
        return m_Height;
    }    
    public int GetArea()
    {    
        return m_Width * m_Height;    
    }    
}
class Square: Rectangle 
{    
    public void SetWidth(int width)
    {    
        m_Width = width;        
        m_Height = width;    
    }
    public void SetHeight(int height)
    {    
        m_Width = height;        
        m_Height = height;    
    }
}

Następnie obliczmy pole:

Rectangle r = new Square();
r.SetWidth(5);    
r.SetHeight(10);        
int area = r.GetArea();

Użytkownik spodziewa się wyniku 50 a otrzymuje 100. Jest to przykład łamania zasady Liskov – sekwencja kodu przynosi różne efekty w zależności od tego czy posiadamy referencję na klasę bazową czy potomną. Klasa potomna nie powinna zmieniać zachowania klasy.

Zasady S.O.L.I.D – Open/closed principle

Zasada O\C mówi, że oprogramowanie powinno być otwarte na rozszerzenia a zamknięte na modyfikacje. Innymi słowy programista powinien być w stanie uzyskać zamierzony efekt poprzez rozszerzenie klasy czy przeładowanie metody a nie zmianę już istniejącego kodu. Zasada jest szczególnie istotna w przypadku kodu produkcyjnego, w którym wszelkie możliwości modyfikacji kodu są ograniczone. Zasada pozwala budować modularne systemy. Użycie ENUM moim zdaniem jest złamaniem Open\Closed principle. Wykorzystując w kodzie typy enumeryczne oraz instrukcje sterujące zamykamy w pewnym sensie możliwości rozszerzenia systemu. Wprowadzenie nowych opcji wymaga bowiem modyfikacji typu enumerycznego. Nie oznacza to oczywiście, że zawsze należy unikać ENUM. Często jest to najwygodniejsze rozwiązanie oraz liczba podtypów jest z góry znana (np. formatowanie tekstu – mamy zbiór zamknięty możliwych opcji).

Pod adresem http://www.oodesign.com/open-close-principle.html znajduje się krótki przykładzik obrazujący złamanie zasady.

Zasady S.O.L.I.D – Single Responsibility Principal

W Inżynierii oprogramowania SOLID oznacza zestaw podstawowych zasad projektowania oprogramowania. Każda literka w wyrazie jest skrótem do jakieś zasady. ‘S’ oznacza Single Responsibility Principal. Podejrzewam, że większość osób doskonale zna już tą zasadę. Aby jednak zachować pewien porządek na blogu będę tłumaczył nawet te oczywiste reguły:).

W skrócie zasada mówi, że każdy obiekt (klasa) powinien być odpowiedzialny za jak najmniejszy fragment logiki. Niedopuszczalne jest aby klasa wykonywała dwie niezależne od siebie funkcje. Robert Martin (autor SRP) definiuje odpowiedzialność jako “reason to change”. Innymi słowy, każda klasa powinna mieć wyłącznie jeden powód do zmiany. Jeśli brzmi to zbyt abstrakcyjnie to rozważmy akademicki przykład klasy wykonującej jakieś obliczenia (choćby mnożenie macierzy – obojętnie). Jeśli taka klasa oprócz wykonywania obliczeń odpowiedzialna jest również za wyświetlenie wyników z pewnością nie spełnia zasady SRP.

Ostatnie pytanie brzmi: jak weryfikować zgodność kodu z SRP? Przede wszystkim należy to do obowiązków programisty. Żadne narzędzia w pełni nie zagwarantują poprawności. Nie mniej jednak, można wspomóc się kilkoma metrykami kodu. Za pomocą narzędzia nDepend można wyliczyć metrykę efferent coupling, która określa jak wiele zewnętrznych klas wywołuje badany fragment kodu. Gdy CE jest zbyt wysokie istnieje prawdopodobieństwo, że klasa nie spełnia SRP. Przydatne również są wszystkie metryki mierzące liczbę powiązań (np. ABC).

Na koniec należy dodać, że nie wszystkie klasy w programie muszą spełniać SRP. Warstwa usług czy wszelkie implementacje wzorca fasady z definicji przecież nie są zgodne z SRP – mają za zadanie skupiać szeroką funkcjonalność w jednym miejscu.

Pętla (ciekawostka)

Pewien kolega podesłał mi dziś ciekawy fragment kodu (znaleziony na jakimś blogu):

int level = 10;
Func<int, int> nextLevel = new Func<int, int>(x => x--);
Func<int, bool> loopAgain = new Func<int, bool>(x => x >= 0);

while (loopAgain(level))
{
    level = nextLevel(level);
}

Pytanie: ile razy wykona się pętla? Odpowiedzi nie będę zdradzał. Zawsze można odpalić i przekonać się samemu:).

W jaki sposób interpretowane są wyrażenia lambda?

Kiedyś czytając książkę “More Effective C#” zaciekawiło mnie wyjaśnienie interpretacji wyrażeń lambda przez kompilator. W książce autor przedstawił następujący fragment kodu:

public class ModFilter
{
    private readonly int modulus;
    public ModFilter(int mod)
    {
        modulus = mod;
    }
    public IEnumerable<int> FindValues(
        IEnumerable<int> sequence)
    {
        return from n in sequence
               where n % modulus == 0
               select n * n; 
    }
}

W rzeczywistości wyrażenie zostanie skonwertowane do znanych już od dawna delegat:

public class ModFilter
{
    private readonly int modulus;
    
    private bool WhereClause(int n)
    {
        return ((n % this.modulus) == 0);
    }
    
    private static int SelectClause(int n)
    {
        return (n * n);
    }
    
    private static Func<int, int> SelectDelegate;
    public IEnumerable<int> FindValues(
        IEnumerable<int> sequence)
    {
        if (SelectDelegate == null)
        {
            SelectDelegate = new Func<int, int>(SelectClause);
        }
        return sequence.Where<int>(
            new Func<int, bool>(this.WhereClause)).
            Select<int, int>(SelectClause);
    }    
}

Kompilator wygenerował metody WhereClause oraz SelectClause. Ciekawszą jednak obserwacją jest przypadek w którym wykorzystujemy zmienne lokalne:

public class ModFilter
{
    private readonly int modulus;    
    public ModFilter(int mod)
    {
        modulus = mod;
    }
    public IEnumerable<int> FindValues(
        IEnumerable<int> sequence)
    {
        int numValues = 0;
        return from n in sequence
               where n % modulus == 0               
               select n * n / ++numValues;
    }
}

Zmienna numValues jest lokalna. W jaki sposób zostanie to zinterpretowane? Otóż kompilator wygeneruje klasę zagnieżdżoną:

public class ModFilter
{
    private sealed class Closure
    {
        public ModFilter outer;
        public int numValues;
        public int SelectClause(int n)
        {
            return ((n * n) / ++this.numValues);
        }
    }
    private readonly int modulus;
    public ModFilter(int mod)
    {
        this.modulus = mod;
    }
    private bool WhereClause(int n)
    {
        return ((n % this.modulus) == 0);
    }
    public IEnumerable<int> FindValues
        (IEnumerable<int> sequence)
    {
        Closure c = new Closure();
                c.outer = this;
        c.numValues = 0;
        return sequence.Where<int>
            (new Func<int, bool>(this.WhereClause))
            .Select<int, int>(
                new Func<int, int>(c.SelectClause));
    }
}

Closure posiada dostęp do zewnętrznej klasy ModFilter. Wszystkie zmienne lokalne (w tym przypadku numValues) zostały po prostu przekopiowane. Używając wyrażeń lambda łatwo zapomnieć o tym, że tak naprawdę tworzone są osobne metody i wszystkie parametry lokalne muszą zostać przekopiowane. Wyrażenia znacznie ułatwiają i przyśpieszają pisanie kodu ale z drugiej strony równie łatwo można popełnić błąd. Autor książki (Bill Wagner) przytacza następujący kod jako przestrogę:

int index = 0;
Func<IEnumerable<int>> sequence =
    () => Utilities.Generate(30, () => index++);
index = 20;
foreach (int n in sequence())
    Console.WriteLine(n);
Console.WriteLine("Done");
index = 100;
foreach (int n in sequence())
    Console.WriteLine(n);

Kod wydrukuje wartości od 20-50 a następnie od 100-130. Według autora może to być zaskakujące ponieważ funkcja definiowana jest dla index=0. Jednak ze względu na to sposób interpretacji (closure) wartość zmiennej jest przekopiowywana i generowanie nie zaczyna się od 0 a od 20 oraz 100. Moim zdaniem jest to akurat bardzo intuicyjne, aczkolwiek należy zdawać sobie sprawę ze sposobu realizacji lambda w C#.

MaxLength w EntityFramework oraz Text Template Transformation Toolkit (T4).

Podczas generowania modelu encji na podstawie bazy danych, EF potrafi pobrać maksymalną długość pola. Jeśli kolumna w bazie posiada typ nvarchar(50) to EF zmapuje to na zmienną typ string oraz ustawi pole MaxLength na wartość 50.

image

Informacja o dozwolonej długości (50) jest zapisana w metadanych. Niestety próba przypisania wartości dłuższej niż 50 znaków zakończy się powodzeniem ponieważ nvarchar(50) został zmapowany na string, który nie ma ograniczenia do 50 znaków. Poniższy kod NIESTETY zadziała:

Contact contact = new Contact();
contact.Email="jakis napis dluzszy niz 50 znakow..."

W jaki sposób walidować więc  dane? Gdy przekażemy napis dłuższy niż 50 znaków wygenerowana encja nie wyrzuci wyjątku jednak podczas zapisu informacji danych do bazy danych oczywiście wyjątek zostanie zwrócony:

String or binary data would be truncated.

Należy zatem w jakiś sposób walidować dane podczas próby przypisania ich do konkretnych właściwości. Jednym ze sposobów jest wykorzystanie klas częściowych. Można w końcu stworzyć klasę, która będzie walidowała dane np:

public partial class Contact
{
   public IEnumerable<RuleViolation> GetRuleViolations()
   {
       if (Email.Length>50)
           yield return new RuleViolation("Nieprawidłowa długość pola email.", "Email");
   }
   public void Validate()
   {
       if (GetRuleViolations().Count() != 0)
           throw new Validation.ValidationException("Encja zawiera nieprawdiłowe dane");
   }
}

gdzie RuleViolation to:

public class RuleViolation
{
   public string ErrorMessage { get; private set; }
   public string PropertyName { get; private set; }

   public RuleViolation(string errorMessage, string propertyName)
   {
       ErrorMessage = errorMessage;
       PropertyName = propertyName;
   }
}

Rozwiązanie ma zasadniczą wadę – każde pole trzeba ręcznie sprawdzać. Jak już wspomniałem EF posiada w metadanych MaxLength więc pomyślny jak można wykorzystać już zapisane dane.

Rozwiązaniem jest tzw. Text Template Transformation Toolkit (T4). T4 jest silnikiem automatycznego generowania kodu. Za pomocą szablonów T4 możemy generować klasy, metody itp. Szablony tworzone są za pomocą specjalnego języka dyrektyw przypominającego nieco dyrektywy ASP .NET. Wykonanie T4 składa się z następujących etapów:

image

Po skompilowaniu szablonu zostanie stworzona klasa GeneratedTextTransformation (wraz z przeładowaną metodą TransformText) odpowiedzialna za wygenerowanie kodu opisanego przez Text Template. Nie będę w tym poście opisywał dokładnie składni T4 ponieważ jest to zdecydowanie temat wymagający przynajmniej kilku postów. Przedstawię tylko przykładowy szablon:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<#Write("class HelloClass\n{");#>   
    public void HelloWorld()
    {
            
    }
}

Kod wygenerowany na podstawie powyższego szablonu:

class HelloClass
{   
    public void HelloWorld()
    {            
    }
}

Wróćmy do głównego tematu. Klasy encji EF generowane są właśnie na podstawie T4. Aby zobaczyć kod T4 wystarczy kliknąć prawym guzikiem myszki na modelu encji (np. plik Entities.edmx) i wybrać Add Code Generation Item. Pojawi się okno w którym zaznaczamy ADO .NET EntityObject Generator.

image

Powstanie plik o rozszerzeniu .tt. Nie będę tutaj wklejał jego zawartości ponieważ jest zdecydowanie zbyt długa :). Szablon generuje klasy wszystkich encji. Jeśli szablon został nazwany Model1.tt to Model1.cs będzie zawierał wygenerowane klasy encji w C#.

Modyfikując szablon możemy więc wpływać na zachowanie generowanych encji. Nie ma więc problemu abyśmy dopisali logikę walidacyjną odpowiedzialną za sprawdzanie długości string’a. W tej chwili szablon generuje właściwości w następujący sposób:

[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
[DataMemberAttribute()]
public global::System.String Email
{
  get
  {
      return _Email;
  }
  set
  {        
      OnEmailChanging(value);

      ReportPropertyChanging("Email");
      _Email = StructuralObject.SetValidValue(value, true);
      ReportPropertyChanged("Email");
      OnEmailChanged();
  }
}

Z kolei my chcemy aby przypisanie właściwości wyglądało:

   [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
        [DataMemberAttribute()]
        public global::System.String Email
        {
            get
            {
                return _Email;
            }
            set
            {                                
                if (value!=null&& value.Length > 50) 
                { 
                   throw new PoliticiansPromises.Entities.Validation.ValidationPropertyException(String.Format("Pole {0} może mieć maksymalnie {1} znaków.",PoliticiansPromises.Dictionaries.PropertyMappingsManager.Instance["Email"], "50"),Email);
                }
    
                OnEmailChanging(value);
    
                ReportPropertyChanging("Email");
                _Email = StructuralObject.SetValidValue(value, true);
                ReportPropertyChanged("Email");
                OnEmailChanged();
            }
        }

 

Można to zrealizować w łatwy sposób dopisując odpowiedni kod do szablonu T4. Największą chyba trudnością jest znalezienie w tym długim szablonie fragmentu odpowiedzialnego za generowanie setter’a. W każdym razie fragment T4 odpowiedzialny za wygenerowanie logiki walidacyjnej wygląda następująco:

<#+       
if (code.Escape(primitiveProperty.TypeUsage) == "global::System.String")
{           
    string facetName = "MaxLength";
   int maxLength = 0;
   if (Int32.TryParse(primitiveProperty.TypeUsage.Facets[facetName].Value.ToString(), out maxLength))
   {

#>            
        if (value!=null&& value.Length > <#= maxLength.ToString() #>) 
       { 
          throw new PoliticiansPromises.Entities.Validation.ValidationPropertyException(String.Format("Pole {0} może mieć maksymalnie {1} znaków.","<#= code.Escape(primitiveProperty) #>", "<#= maxLength.ToString() #>"),<#=code.Escape(primitiveProperty)#>);
       }
<#+
  }
}                    
                
#>

 

Najpierw sprawdzamy czy właściwość jest typu String a następnie pobieramy z metadanych maksymalną długość. Po dopisaniu powyższego fragmentu w odpowiednim miejscu wszystkie encje zostaną wygenerowane ze stosowną logika walidacyjną. Kod powinien  być dopisany bezpośrednio po następującym fragmencie T4:

 

    <#=code.SpaceAfter(Accessibility.ForSetter((primitiveProperty)))#>set
        {        
<#+
        if (ef.IsKey(primitiveProperty))
            {
                if (ef.ClrType(primitiveProperty.TypeUsage) == typeof(byte[]))
                {
#>
            if (!StructuralObject.BinaryEquals(<#=code.FieldName(primitiveProperty)#>, value))
<#+
                }
                else
                {
#>
            if (<#=code.FieldName(primitiveProperty)#> != value)
<#+
                }
#>
            {
<#+
        PushIndent(CodeRegion.GetIndent(1));
            }
#>

 

Od teraz programista po dodaniu nowej encji nie musi pisać żadnego kodu odpowiedzialnego za walidacje długości samodzielnie.

Różnica miedzy IEnumarable a IQueryable (LINQ to SQL)

Interfejsy IEnumerable oraz IQueryable mogą wydawać się bardzo podobne. W końcu IQueryable implementuje IEnumerable więc funkcjonalność musi być podobna. W praktyce poniższe dwa zapytania bardzo się różnią:

IEnumerable<Customer> customers = context.Customers.Where(c => c.FirstName.StartsWith("J"));
customers = customers.Take<Customers>(5);

IQueryable<Customer> customers = context.Customers.Where(c => c.FirstName.StartsWith("J"));
customers = customers.Take<Customers>(5);

Pierwsze zapytanie jest skrajnie niewydajne. Najpierw zostaną wybrani wszyscy klienci z imieniem zaczynającym się od litery J a dopiero potem lokalnie zostanie zwróconych pierwszych 5 wierszy. Jeśli zatem tabela zawiera milion wierszy to wszystkie  te wiersze zostaną odczytane z bazy danych. W SQL wyglądałoby to następująco:

select * from Customers WHERE FirstName like 'j%';

W pamięci powstanie tablica zawierająca milion elementów. Następnie metoda Take zwróci pierwszych 5 elementów.

W przypadku interfejsu IQueryable zapytanie wygląda juz w pełni optymalnie tzn:

select top(5) * from Customers WHERE FirstName like 'j%';

Należy zwrócić uwagę na top(5) – z bazy danych zostaną odczytane wyłącznie te wiersze, które tak naprawdę chcemy. Z przykładów widać, że bardzo łatwo napisać niewydajny kod. Wszelkie wywołania IEnumerable są wykonywane lokalnie i nie zostaną przetworzone bezpośrednio w zapytanie SQL.