Zagadnienia egzaminu 70-483 opisane w tej notatce:
  • Zapytania LINQ - pisanie zapytań i metod z użyciem LINQ.
  • Manipulowanie danymi za pomocą LINQ - manipulowanie danymi za pomocą LINQ a w tym użycie projekcji, użycie złączeń, grupowanie kolekcji, metody agregujące, pobieranie określonej ilości wyników i pomijanie określonej liczby wyników.
  • Użycie LINQ to XML - tworzenie i modyfikacja danych z użyciem LINQ to XML.

Wyrażenia zapytan LINQ

Language Integrated Query (LINQ) jest aspektem języków w .NET Framework który umożliwia użycie ogólnej składni zapytań do wykonywania zapytań na kolekcjach, dokumentach XML, bazach danych oraz wszystkich typach wspierających interfejsy IEnumerable< T > lubIQueryable< T >
Istnieją dwa rodzaje składni, które wykonują zapytania LINQ. Pierwszym są wyrażenia zapytań. Drugim są zapytania oparte o metody. Dla kompilatora wybór między tymi dwoma metodami jest nieistotny, należy wybrać metodę która bardziej nam odpowiada.

Kompilator konwertuje wyrażenia zapytań na zapytania oparte o metody w czasie kompilowania. Wyrażenia zapytań są czasem łatwiejsze do odczytu lecz istnieją operacje których nie można wykonać za pomocą wyrażeń zapytań.

Wyrażenia zapytań szukają w danych zapisanych w tablicy, kolekcji lub jakimkolwiek typie wspierającym interfejsy IEnumerable< T > lub IQueryable< T >. Składnia wyrażeń zapytań jest podobna do języka SQL.

Zanim powstało LINQ do wyszukiwania danych wykorzystywane były pętle. Poniższy przykład demonstruje wyszukanie parzystych liczb w tablicy za pomocą pętli foreach:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int[] evenNumbers = newint[5];
int evenIndex = 0;

foreach (int i in myArray)
{
if (i % 2 == 0)
{
evenNumbers[evenIndex] = i;
evenIndex++;
}
}

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

Wyrażenia zapytań LINQ pozwalają na wykonanie zapytania na kolekcji z użyciem składni podobnej do SQL z tym wyjątkiem, że w C# kolejność wyrażeń jest inna. Korzyścią z użycia LINQ jest mniejsza ilość kodu i większa jego czytelność. 
Poniższy fragment kodu wykonuje zapytanie LINQ na tablicy pobierające liczby parzyste:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = from i in myArray
where i % 2 == 0
select i;

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

Zmienna evenNumbers została zadeklarowana jako var. Zmienna zdefiniowana jako var jest nazywana zmienną typu niejawnego. Oznacza to, że kompilator określa typ zmiennej bazując na wyrażeniu po prawej stronie instrukcji inicjującej. W tym wypadku kompilator wie, że wartości w zmiennej evenNumbers są typu int.

Klauzula from zawiera i in myArray. Kompilator niejawnie wie jakiego typu jest zmienna i bazując na type myArray. Jest to ekwiwalent instrukcji pętli foreach(int i in myArray). Zmienna i reprezentuje wyliczane wartości w tablicy.

W składni LINQ pierwszą klauzulą jest from, drugą where a ostatnią select. W klauzuli where należy używać operatora równoważności (==).

Klauzula select zwraca zmienną i. W tym wypadku oznacza to, że kod wyliczając tablicę powinien zwrócić wszystkie elementy spełniające warunek klauzuli where. Wykonując kod krokowo można zauważyć, że w momencie wykonywania pętli foreach kod powróci do zapytania LINQ. Dzieje się tak ponieważ zapytanie LINQ nie zostało wykonane do momentu kiedy zmienna evenNumbers została użyta. Nazywa się to wykonaniem odroczonym (ang. deferred execution). Jeżeli podejrzymy zmienną evenNumbers w oknie podglądu w czasie wykonywania to zauważymy, że nie posiada ona wartości i, że jest wykonywana za każdym razem kiedy elementy są wyliczane. Jeżeli źródło danych zostanie zmienione i wyliczymy elementy jeszcze raz to wynik będzie inny, np:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = from i in myArray
where i % 2 == 0
select i;

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

myArray[1] = 12;

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

Filtrowanie

Filtrowanie danych jest dokonywane z użyciem klauzuli where w zapytaniu. Ponieważ używamy języka C# to do złożonych operacji używamy operatorów and (&&) i or (||). W poprzednim przykładzie klauzula where zawiera wyrażenie i % 2 == 0. Takie wyrażenie nazywane jest predykatem. Predykat jest wyrażeniem porównującym które jest wykonywane dla każdego elementu w sekwencji. 
Poniższy fragment kodu zwraca wszystkie liczby parzyste większe niż pięć z tablicy liczb:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = from i in myArray
where i % 2 == 0 && i > 5
select i;

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

Można napisać wiele klauzul where w wyrażeniu zapytania. Oznacza to to samo jakbyśmy mieli wiele wyrażeń w klauzuli where złączonych operatorem &&
Poniższy kod produkuje ten sam wynik co poprzedni:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = from i in myArray
where i % 2 == 0
where i > 5
select i;

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

Dla bardziej skomplikowanych warunków wymagających operatorów pierwszeństwa należy użyć nawiasów okrągłych ().

Należy zwrócić szczególna uwagę na to, że w wyrażeniu zapytania można użyć wywołania metod. 
Poniższy fragment kodu wywołuje metodę IsEvenAndGT5 w wyrażeniu zapytania:

staticvoidRetrieveEvenNumberGT5V3()
{
int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = from i in myArray
where IsEvenAndGT5(i)
select i
;
foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}
}

staticboolIsEvenAndGT5(int i)
{
return (i % 2 == 0 && i > 5);
}

Sortowanie

Wyniki działania zapytania można sortować za pomocą klauzuli orderby. Można sortować rosnąco i malejąco tak jak w języku SQL
Poniższy fragment kodu sortuje parzyste wyniki w kolejności malejącej:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = from i in myArray
where i % 2 == 0
orderby i descending
select i;

foreach (int i in evenNumbers)
{
Debug.WriteLine(i);
}

Można również sortować więcej właściwości poprzez oddzielenie ich przecinkiem. 
Poniższy fragment kodu sortuje zwracane elementy alfabetycznie wg stanu a potem wg miasta:

classHometown
{
publicstring City { get; set; }
publicstring State { get; set; }
}

staticvoidOrderByStateThenCity()
{
List<Hometown> hometowns = new List<Hometown>()
{
new Hometown() { City = "Philadelphia", State = "PA" },
new Hometown() { City = "Ewing", State = "NJ" },
new Hometown() { City = "Havertown", State = "PA" },
new Hometown() { City = "Fort Washington", State = "PA" },
new Hometown() { City = "Trenton", State = "NJ" }
};

var orderedHometowns = from h in hometowns
orderby h.State ascending, h.City ascending
select h;

foreach (Hometown hometown in orderedHometowns)
{
Debug.WriteLine(hometown.City + ", " + hometown.State);
}
}

Projekcja

Klauzula select zwraca obiekty w sekwencji lub zwraca określoną liczbę właściwości obiektu w sekwencji. Pobranie określonej liczby właściwości lub transformacja wyniku do innego typu nazywana jest projekcją wyniku. 
Poniższy fragment kodu demonstruje pobranie nazwisk osób z listy obiektów klasy Person:

classPerson
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicstring Address1 { get; set; }
publicstring City { get; set; }
publicstring State { get; set; }
publicstring Zip { get; set; }
}

privatestaticvoidProjectionV1()
{
List<Person> people = new List<Person>()
{
new Person()
{
FirstName = "John",
LastName = "Smith",
Address1 = "First St",
City = "Havertown",
State = "PA",
Zip = "19084"
},
new Person()
{
FirstName = "Jane",
LastName = "Doe",
Address1 = "Second St",
City = "Ewing",
State = "NJ",
Zip = "08560"
},
new Person()
{
FirstName = "Jack",
LastName = "Jones",
Address1 = "Third St",
City = "Ft Washington",
State = "PA",
Zip = "19034"
}
};

var lastNames = from p in people
select p.LastName;

foreach (string lastName in lastNames)
{
Debug.WriteLine(lastName);
}
}
Klauzula select pobiera p.LastName zamiast całego obiektu p. Kompilator ustala, że wynikiem powinna być lista typu string bazując na typie pobieranej właściwości.

Załóżmy teraz, że potrzebujemy zwrócić imię i nazwisko osób z listy. 
Poniższy fragment kodu demonstruje pobranie imion i nazwisk osób z listy obiektów. zapytanie to tworzy typ anonimowy zawierający właściwości FirstName i LastName:

var names = from p in people
selectnew { p.FirstName, p.LastName };

foreach (var name in names)
{
Debug.WriteLine(name.FirstName + ", " + name.LastName);
}

Właściwości typu anonimowego można jawnie nazwać używając poniższej składni:

var names = from p in people
selectnew { First = p.FirstName, Last = p.LastName };

foreach (var name in names)
{
Debug.WriteLine(name.First + ", " + name.Last);
}

Złączenia

Do połączenia dwóch lub więcej sekwencji używana jest klauzula join
Poniższy fragment kodu łączy dwie oddzielne listy po polu StateId:

classEmployee
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicint StateId { get; set; }
}

classState
{
publicint StateId { get; set; }
publicstring StateName { get; set; }
}

staticvoidJoin()
{
List<Employee> employees = new List<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
StateId = 2
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
StateId = 1
}
};

List<State> states = new List<State>()
{
new State()
{
StateId = 1,
StateName = "PA"
},
new State()
{
StateId = 2,
StateName = "NJ"
}
};

var employeeByState = from e in employees
join s in states
on e.StateId equals s.StateId
selectnew { e.LastName, s.StateName };

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.StateName);
}
}

Klauzula join używa operatora equal zamiast =. Łączone pola mogą być porównywane tylko tym operatorem. Nie można używać operatorów < i > tak jak w języku SQL.

Outer Join

Aby napisać zapytanie LINQ równorzędne z zapytaniem SQL używającym LEFT OUTER JOIN lub RIGHT OUTER JOIN należy użyć słowa kluczowego group join i metody DefaultIfEmpty. Słowo kluczowe group join pozwala połączyć dwie sekwencje i wstawić je do trzeciej sekwencji.

Dodajmy obiekt Employee do listy employees tak aby wartość pola StateId nie istniała w liście states:

new Employee()
{
FirstName = "Sue",
LastName = "Smith",
StateId = 3
}

Poniższy kod demonstruje pobranie wszystkich elementów z listy employees nawet jeżeli nie ma dopasowania na liście states:

var employeeByState = from e in employees
join s in states
on e.StateId equals s.StateId into employeeGroup
from item in
employeeGroup.DefaultIfEmpty(
new State { StateId = 0, StateName = string.Empty })
selectnew { e.LastName, item.StateName };

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.StateName);
}

Kombinacja list jest wstawiana do obiektu nazwanego employeeGroup poprzez użycie słowa kluczowego into. Druga klauzula from tworząca nową instancję obiektu State z wartościami StateId równą 0 i pustym ciągiem StateName kiedy nie istnieje dopasowanie po polu StateId. Kiedy używamy klauzuli into to nie możemy więcej odnieść się do zmiennej po prawej stronie klauzuli on. Zamiast tego używamy zmiennej która wylicza wartości do nowej sekwencji, w przykładzie jest to zmienna item.

Pisząc wyrażenia zapytań możemy wykonywać tylko złączenia lewostronne left join dla których kolejność zapisu jest istotna.

Klucze złożone

Czasami zachodzi sytuacja w której musimy dokonać złączenia sekwencji za pomocą więcej niż jednej właściwości. Aby tego dokonać tworzymy typ anonimowy zawierający właściwości które chcemy porównać w klauzuli join.

Przykładowo zmieńmy klasę Hometown tak aby posiadała właściwość CityCode oraz zmieńmy klasę Employee tak aby posiadała właściwości City i State a nie posiadała StateId:

classHometown
{
publicstring City { get; set; }
publicstring State { get; set; }
publicstring CityCode { get; set; }
}

classEmployee
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicstring City { get; set; }
publicstring State { get; set; }
}

Poniższy fragment kodu złącza dwie sekwencje używając właściwości City i State:

List<Employee> employees = newList<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
City = "Havertown",
State = "PA"
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
City = "Ewing",
State = "NJ"
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
City = "Fort Washington",
State = "PA"
}
};

List<Hometown> hometowns = newList<Hometown>()
{
new Hometown()
{
City = "Havertown",
State = "PA",
CityCode = "1234"
},
new Hometown()
{
City = "Ewing",
State = "NJ",
CityCode = "5678"
},
new Hometown()
{
City = "Fort Washington",
State = "PA",
CityCode = "9012"
}
};

var employeeByState = from e in employees
join h in hometowns
onnew { City = e.City, State = e.State } equals
new { City = h.City, State = h.State }
selectnew { e.LastName, h.CityCode };

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.CityCode);
}

W klauzuli join utworzone zostały dwa typy anonimowe z tymi samymi właściwościami. Równoważność jest określana poprzez porównanie wszystkich właściwości typów anonimowych.

Grupowanie

Bardzo często potrzeba nam pogrupować obiekty w celu zliczenia sum dla poszczególnych właściwości. Przykładowo możemy wykonać raport wyświetlający liczbę pracowników w każdym stanie. Aby tego dokonać można użyć klauzuli group w wyrażeniu zapytania. 
Poniższy fragment kodu tworzy listę pracowników i grupuje ich wg stanu. Liczba pracowników dla każdego stanu jest wyświetlana w oknie Output:

privatestaticvoidGroup()
{
List<Employee> employees = new List<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
City = "Havertown",
State = "PA"
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
City = "Ewing",
State = "NJ"
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
City = "Fort Washington",
State = "PA"
}
};

var employeesByState = from e in employees
group e by e.State;

foreach (var employeeGroup in employeesByState)
{
Debug.WriteLine(employeeGroup.Key + ": " + employeeGroup.Count());

foreach (var employee in employeeGroup)
{
Debug.WriteLine(employee.LastName + ", " + employee.State);
}
}
}

W powyższym przykładzie nie ma klauzuli select. Jest to możliwe dlatego, że klauzula group zwraca kolekcję IGrouping<TKey,TElement>. Jest to kolekcja zawierająca właściwość dla klucza po którym sekwencja została pogrupowana. 
W powyższym przykładzie są dwie pętle foreach. Pierwsza iteruje po kolekcji IGrouping i wyświetla właściwość Key oraz Count jako liczbę elementów w grupie. Wewnętrzna pętla iteruje po elementach każdej grupy i wyświetla nazwisko i stan dla każdego pracownika w grupie.

W klauzuli group by można dodać logikę grupującą po dowolnych właściwościach. 
Poniższy przykład grupuje liczby parzyste i nieparzyste oraz wyświetla ich liczbę:

staticvoidGroupV2()
{
int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var groupedNumbers = from i in myArray
group i by(i % 2 == 0 ? "Even" : "Odd")
;

foreach (var groupNumber in groupedNumbers)
{
Debug.WriteLine(groupNumber.Key + ": " + groupNumber.Sum());
foreach(var number in groupNumber)
{
Debug.WriteLine(number);
}
}
}

Grupując sekwencje można również użyć klauzuli select, ale trzeba również użyć słowa kluczowego into w klauzuli group
Poniższy fragment kodu demonstruje wyświetlenie liczb parzystych i nieparzystych oraz ich liczby:

privatestaticvoidGroupV3()
{
int[] myArray = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var groupedNumbers = from i in myArray
group i by i % 2 == 0 ? "Even" : "Odd"
into g select new { Key = g.Key, SumOfNumbers = g.Sum() };

foreach (var groupNumber in groupedNumbers)
{
Debug.WriteLine(groupNumber.Key + ": " + groupNumber.SumOfNumbers);
}
}

Zapytania oparte o metody

Wszystkie instrukcje które można wyrazić za pomocą wyrażeń zapytań LINQ można również napisać za pomocą zapytań LINQ opartych o metody. Funkcjonalnie są one równoważne lecz posiadają inną składnię.

Zapytania LINQ oparte o metody są metodami rozszerzającymi znajdującymi się w przestrzeni nazw System.Linq. Metody te rozszerzają wszystkie typy implementujące interfejsy IEnumerable lub IQueryable. Metody zapytań jako parametr przyjmują wyrażenia lambda, które reprezentują logikę która ma zostać wykonana w czasie enumeracji sekwencji. 
Przypomnijmy prosty przykład wyrażenia zapytania LINQ które zwraca parzyste liczby z tablicy:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = from i in myArray
where i % 2 == 0
select i;

Równoważne zapytanie oparte o metody rozszerzające LINQ wygląda następująco:

int[] myArray = newint[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = myArray.Where(i => i % 2 == 0);

Zmienna i reprezentuje element w tablicy a kod po prawej stronie operatora => reprezentuje logike wykonywaną w czasie enumeracji tablicy.

Filtrowanie

Filtrowanie danych odbywa się z użyciem metody Where do której przekazujemy wyrażenie lambda jako parametr które zwraca wartość typu bool a metoda zwraca tylko te elementy sekwencji które spełniają warunek, np:

myArray.Where(i => i % 2 == 0)

W metodzie Where można używać operatorów (&&) lub (||), np:

var evenNumbers = myArray.Where(i => i % 2 == 0 && i > 5);

Jeżeli wymagamy operatora pierwszeństwa operacji to zamiast niego możemy użyć wielu metod Where, np:

var evenNumbers = myArray.Where(i => i % 2 == 0).Where(i => i > 5);

Sortowanie

Sekwencje można sortować używając metod OrderBy lub OrderByDescending, np:

var evenNumbers = myArray.Where(i => i % 2 == 0).OrderByDescending(i => i);

Jeżeli musimy sortować po więcej niż jednej właściwości służą do tego metody ThenBy lub ThenByDescending, np:

staticvoid MethodBasedOrderByStateThenCity()
{
List<Hometown> hometowns = newList<Hometown>()
{
new Hometown() { City = "Philadelphia", State = "PA" },
new Hometown() { City = "Ewing", State = "NJ" },
new Hometown() { City = "Havertown", State = "PA" },
new Hometown() { City = "Fort Washington", State = "PA" },
new Hometown() { City = "Trenton", State = "NJ" }
};

var orderedHometowns = hometowns.OrderBy(h => h.State).ThenBy(h => h.City);

foreach (Hometown hometown in orderedHometowns)
{
Debug.WriteLine(hometown.City + ", " + hometown.State);
}
}

Projekcja

Można dokonywać projekcji obiektów za pomocą metody Select, np:

private static void MethodBasedProjectionV1()
{
List<Person> people = new List<Person>()
{
new Person()
{
FirstName = "John",
LastName = "Smith",
Address1 = "First St",
City = "Havertown",
State = "PA",
Zip = "19084"
},
new Person()
{
FirstName = "Jane",
LastName = "Doe",
Address1 = "Second St",
City = "Ewing",
State = "NJ",
Zip = "08560"
},
new Person()
{
FirstName = "Jack",
LastName = "Jones",
Address1 = "Third St",
City = "Ft Washington",
State = "PA",
Zip = "19034"
}
};

var lastNames = people.Select(p => p.LastName);

foreach (string lastName in lastNames)
{
Debug.WriteLine(lastName);
}
}

W metodzie Select można tworzyć typy anonimowe tak samo jak w wyrażeniach zapytań. Jedyną różnicą jest konieczność użycia wyrażenia lambda. 
Poniższy fragment kodu tworzy typ anonimowy z właściwościami FirstName i LastName:

private static void MethodBasedProjectionV2()
{
List<Person> people = new List<Person>()
{
new Person()
{
FirstName = "John",
LastName = "Smith",
Address1 = "First St",
City = "Havertown",
State = "PA",
Zip = "19084"
},
new Person()
{
FirstName = "Jane",
LastName = "Doe",
Address1 = "Second St",
City = "Ewing",
State = "NJ",
Zip = "08560"
},
new Person()
{
FirstName = "Jack",
LastName = "Jones",
Address1 = "Third St",
City = "Ft Washington",
State = "PA",
Zip = "19034"
}
};

var names = people.Select(p => new { p.FirstName, p.LastName });

foreach (var name in names)
{
Debug.WriteLine(name.FirstName + ", " + name.LastName);
}
}

Nazwy właściwości typów anonimowych można jawnie określać, np:

var names = people.Select(p => new { First = p.FirstName, Last = p.LastName });

Istnieje również metoda SelectMany którą można wykorzystać do spłaszczenia dwóch sekwencji do jednej sekwencji podobnie do działania join
Poniższy przykład spłaszcza listy pracowników i stanów i zwraca kombinację tych list:

private staticvoid MethodBasedProjectionV4()
{
List<Employee> employees = newList<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
StateId = 2
},
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
}
};

List<State> states = newList<State>()
{
new State() { StateId = 1, StateName = "PA" },
new State() { StateId = 2, StateName = "NJ" }
};

var employeeByState =
employees.SelectMany(
e => states.Where(s => e.StateId == s.StateId)
.Select(s => new { e.LastName, s.StateName }));

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.StateName);
}
}

Złączenia

Metoda Join pozwala na złączenie dwóch sekwencji używając wspólnej właściwości lub zestawu właściwości. 
Poniższy fragment kodu łączy listę pracowników z listą stanów używając właściwości StateId:

private staticvoid MethodBasedJoin()
{
List<Employee> employees = newList<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
StateId = 2
},
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
}
};

List<State> states = newList<State>()
{
new State() { StateId = 1, StateName = "PA" },
new State() { StateId = 2, StateName = "NJ" }
};

var employeeByState = employees.Join(
states,
e => e.StateId,
s => s.StateId,
(e, s) => new { e.LastName, s.StateName });

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.StateName);
}
}

Lista employees jest rozważana jako sekwencja zewnętrzna. Pierwszym parametrem metody Join jest sekwencją którą chcemy złączyć, w przykładzie states, która jest rozważana jako sekwencja wewnętrzna. Drugim parametrem jest klucz złączenia sekwencji zewnętrznej. Trzecim parametrem jest klucz złączenia sekwencji wewnętrznej. Domyślnie użyte zostanie porównanie równoważności sekwencji. Czwartym parametrem jest wyrażenie lambda tworzące wynik złączenia.

Outer Join

Złączenia typu Outer Join są realizowane za pomocą metody GroupJoin
Poniższy przykład realizuje left join list pracowników i stanów. Jeżeli nie ma połączenia w liście stanów to nazwa stanu będzie pusta:

privatestatic void MethodBasedOuterJoin()
{
List<Employee> employees = newList<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
StateId = 2
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
StateId = 1
},
new Employee()
{
FirstName = "Sue",
LastName = "Smith",
StateId = 3
}
};

List<State> states = newList<State>()
{
new State() { StateId = 1, StateName = "PA" },
new State() { StateId = 2, StateName = "NJ" }
};

var employeeByState =
employees.GroupJoin(
states,
e => e.StateId,
s => s.StateId,
(e, employeeGroup) => employeeGroup.Select(
s => new { LastName = e.LastName, StateName = s.StateName })
.DefaultIfEmpty(new { LastName = e.LastName, StateName = string.Empty }))
.SelectMany(e => e);

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.StateName);
}
}

Pierwsze trzy parametry metody GroupJoin są analogiczne do metody Join
Czwarty parametr jest wyrażeniem lambda dla sekwencji zewnętrznej i nowej listy utworzonej analogicznie do słowa kluczowego into używanego w wyrażeniach zapytań, którą dowolnie nazywamy, w przykładzie to employeeGroup
Używamy metody Select do wyliczenia wszystkich wartości w sekwencji employeeGroup i metody DefaultIfEmpty kiedy nie ma dopasowania pomiędzy sekwencjami. Na konie należy wywołać metodę SelectMany aby zwrócić sekwencję obiektów.

Klucze złożone

Klucze złożone tworzymy poprzez typy anonimowe. 
Poniższy fragment kodu demonstruje połączenie sekwencji po dwóch właściwościach:

private staticvoid MethodBasedCompositeKey()
{
List<Employee> employees = newList<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
City = "Havertown",
State = "PA"
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
City = "Ewing",
State = "NJ"
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
City = "Fort Washington",
State = "PA"
}
};

List<Hometown> hometowns = newList<Hometown>()
{
new Hometown()
{
City = "Havertown",
State = "PA",
CityCode = "1234"
},
new Hometown()
{
City = "Ewing",
State = "NJ",
CityCode = "5678"
},
new Hometown()
{
City = "Fort Washington",
State = "PA",
CityCode = "9012"
}
};

var employeeByState = employees.Join(
hometowns,
e => new { City = e.City, State = e.State },
h => new { City = h.City, State = h.State },
(e, h) => new { e.LastName, h.CityCode });

foreach (var employee in employeeByState)
{
Debug.WriteLine(employee.LastName + ", " + employee.CityCode);
}
}

Grupowanie

Metoda GroupBy może zostać użyta do grupowania po jednym lub więcej polu. Jest ona równoważna do uzycia słowa kluczowego group w wyrażeniach zapytań. 
Poniższy fragment kodu grupuje listę pracowników wg stanu:

privatestaticvoidMethodBasedGroupV1()
{
List<Employee> employees = new List<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
City = "Havertown",
State = "PA"
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
City = "Ewing",
State = "NJ"
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
City = "Fort Washington",
State = "PA"
}
};

var employeesByState = employees.GroupBy(e => e.State);

foreach (var employeeGroup in employeesByState)
{
Debug.WriteLine(employeeGroup.Key + ": " + employeeGroup.Count());

foreach (var employee in employeeGroup)
{
Debug.WriteLine(employee.LastName + ", " + employee.State);
}
}
}

Metoda GroupBy zwraca kolekcję IGrouping<TKey, TElement>, która może zostać wyliczana aby wykonać funkcje agregujące na elementach grupy. 
Jeżeli chcemy grupować po więcej niż jednym polu należy użyć typu anonimowego jako parametru metody GroupBy
Poniższy fragment kodu grupuje pracowników wg miasta i stanu:

var employeesByState = employees.GroupBy(e => new { e.City, e.State });

Funkcje agregujące

Funkcje agregujące pozwalają na wykonanie operacji obliczeniowych takich jak averagesumcountmax i min na kolekcjach. Przykładowo mając listę reprezentującą faktury możemy szybko wykonać sumę faktury używając metody Sum. Te funkcje są dostępne jedynie jako metody ale można ich używać również w wyrażeniach zapytań. 
Poniższy fragment kodu demonstruje wyrażenie zapytania oraz jego równoważne zapytanie bazujące na metodach dla przykładowych funkcji agregujących:

count

Wyrażenie:

intcount = (from i in myArray
where i % 2 == 0
select i).Count();

Metoda:

intcount = myArray.Where(i => i % 2 == 0).Count();

Można również odroczyć wykonanie zapytania używając następującej składni:

var evenNumbers = from i in myArray
where i % 2 == 0
select i;

int count = evenNumbers.Count();

average

Wyrażenie:

double average = (from i in myArray
where i % 2 == 0
select i).Average();

Metoda:

double average = myArray.Where(i => i % 2 == 0).Average();

sum

Wyrażenie:

intsum = (from i in myArray
where i % 2 == 0
select i).Sum();

Metoda:

intsum = myArray.Where(i => i % 2 == 0).Sum();

min

Wyrażenie:

intsum = (from i in myArray
where i % 2 == 0
select i).Min();

Metoda:

intsum = myArray.Where(i => i % 2 == 0).Min();

max

Wyrażenie:

intsum = (from i in myArray
where i % 2 == 0
select i).Max();

Metoda:

intsum = myArray.Where(i => i % 2 == 0).Max();

Pierwszy i ostatni element

Istnieją dwie dodatkowe funkcje które umożliwiają odnalezienie pierwszego i ostatniego elementu w sekwencji. Dostępne są jako metody ale można ich również użyć w wyrażeniach zapytań. Funkcje te mogą być użyteczne kiedy chcemy znaleźć w sekwencji pierwszy lub ostatni element który spełnia określony warunek, np pierwszy numer parzysty w sekwencji.

first

Wyrażenie:

int first = (from i in myArray
where i % 2 == 0
select i).First();

Metoda:

int first = myArray.Where(i => i % 2 == 0).First();

last

Wyrażenie:

int first = (from i in myArray
where i % 2 == 0
select i).Last();

Metoda:

int first = myArray.Where(i => i % 2 == 0).Last();

Konkatenacja

Metoda Concat pozwala na złączenie dwóch sekwencji w jedną na zasadzie podobnej do operatora UNION w języku SQL
Poniższy fragment kodu złącza dwie listy pracowników w jedną:

private staticvoid Concat()
{
List<Employee> employees = newList<Employee>()
{
new Employee() { FirstName = "John", LastName = "Smith" },
new Employee() { FirstName = "Jane", LastName = "Doe" },
new Employee() { FirstName = "Jack", LastName = "Jones" }
};

List<Employee> employees2 = newList<Employee>()
{
new Employee()
{
FirstName = "Bill",
LastName = "Peters"
},
new Employee()
{
FirstName = "Bob",
LastName = "Donalds"
},
new Employee()
{
FirstName = "Chris",
LastName = "Jacobs"
}
};

var combinedEmployees = employees.Concat(employees2);

foreach (var employee in combinedEmployees)
{
Debug.WriteLine(employee.LastName);
}
}

Należy uważać ponieważ typy obu złączanych ze sobą list nie muszą byś takie same. Można kombinować różne typy poprzez wybieranie typu anonimowego z każdej sekwencji. 
Poniższy kod tworzy kombinację listy pracowników i listy osób tworząc typ anonimowy który zawiera tylko nazwiska:

private staticvoid ConcatV2()
{
List<Employee> employees = newList<Employee>()
{
new Employee() { FirstName = "John", LastName = "Smith" },
new Employee() { FirstName = "Jane", LastName = "Doe" },
new Employee() { FirstName = "Jack", LastName = "Jones" }
};

List<Person> people = newList<Person>()
{
new Person() { FirstName = "Bill", LastName = "Peters" },
new Person() { FirstName = "Bob", LastName = "Donalds" },
new Person() { FirstName = "Chris", LastName = "Jacobs" }
};

var combinedEmployees =
employees.Select(e => new { Name = e.LastName })
.Concat(people.Select(p => new { Name = p.LastName }));

foreach (var employee in combinedEmployees)
{
Debug.WriteLine(employee.Name);
}
}

Dzielenie sekwencji

Sekwencje można dzielić lub stronicować poprzez użycie metod Skip i Take.

Metoda Skip pobiera jako parametr liczbę całkowitą która określa ilość elementów które ma pominąć w sekwencji. 
Poniższy fragment kody demonstruje pominięcie pierwszego elementu z listy pracowników:

var newEmployees = employees.Skip(1);

Metoda Take pobiera jako parametr liczbę całkowitą która określa ilość elementów które ma pobrać z sekwencji. 
Poniższy fragment kody demonstruje pobranie dwóch elementów z listy pracowników:

var newEmployees = employees.Take(2);

Obie metody mogą być użyteczne przy stronicowaniu wyników zapytania. 
Poniższy fragment kodu demonstruje pobranie trzeciej strony wyników wyświetlanych po 10:

var newEmployees = employees.Skip(20).Take(10);

Usuwanie duplikatów

Metoda Distinct zwraca listę niepowtarzających się wartości z sekwencji. 
Poniższy fragment kodu zwraca niepowtarzające się liczby z tablicy:

int[] myArray = newint[] { 1, 2, 3, 1, 2, 3, 1, 2, 3 };

var distinctArray = myArray.Distinct();

foreach (int i in distinctArray)
{
Debug.WriteLine(i);
}

Aby używać metody Distinct na własnych typach należy implementować w nich interfejs IEquatable ponieważ metoda Distinct używa domyślnego porównywania równości obiektów. Interfejs IEquatable posiada metodę Equals do zaimplementowania która porównuje obiekty. Należy również nadpisać metodę GetHashCode obiektu tak aby zwracała kod skrótu na podstawie właściwości w swoim obiekcie.

Poniższy przykład demonstruje porównanie obiektów niestandardowych:

class State : IEquatable<State>
{
publicint StateId { get; set; }
publicstring StateName { get; set; }

publicboolEquals(State other)
{
if (Object.ReferenceEquals(this, other))
{
returntrue;
}
else
{
if (StateId == other.StateId && StateName == StateName)
{
returntrue;
}
else
{
returnfalse;
}
}
}

public override intGetHashCode()
{
return StateId.GetHashCode() ^ StateName.GetHashCode();
}
}

staticvoidDistinctCodeLab()
{
List<State> states = new List<State>()
{
new State(){ StateId = 1, StateName = "PA"},
new State() { StateId = 2, StateName = "NJ"},
new State() { StateId = 1, StateName = "PA" },
new State() { StateId = 3, StateName = "NY"}
};

var distintStates = states.Distinct();

foreach (State state in distintStates)
{
Debug.WriteLine(state.StateName);
}
}

Wykorzystanie LINQ to XML

LINQ to XML pozwala na łatwą konwersję sekwencji do dokumentu XML
Poniższy fragment kodu konwertuje listę pracowników do XML:

private static void LINQToXML()
{
List<Employee> employees = newList<Employee>()
{
new Employee()
{
FirstName = "John",
LastName = "Smith",
StateId = 1
},
new Employee()
{
FirstName = "Jane",
LastName = "Doe",
StateId = 2
},
new Employee()
{
FirstName = "Jack",
LastName = "Jones",
StateId = 1
}
};

var xmlEmployees = new XElement(
"Root",
from e in employees
select
new XElement(
"Employee",
new XElement("FirstName", e.FirstName),
new XElement("LastName", e.LastName)));

Debug.WriteLine(xmlEmployees);
}

Wynik działania kodu:

<Root>
<Employee>
<FirstName>John</FirstName>
<LastName>Smith</LastName>
</Employee>
<Employee>
<FirstName>Jane</FirstName>
<LastName>Doe</LastName>
</Employee>
<Employee>
<FirstName>Jack</FirstName>
<LastName>Jones</LastName>
</Employee>
</Root>

Klasa XElement znajduje się w przestrzeni nazw System.Xml.Linq. Pierwszym parametrem konstruktora jest nazwa elementu. Drugim parametrem jest tablica parametrów ParamArray, co oznacza, że można przekazać zmienną liczbę argumentów do konstruktora.

Podsumowanie

Language Integrated Query LINQ

  • Obiekty implementujące interfejsy IEnumerable lub IQueryable mogą myć odpytywane za pomocą LINQ.
  • Wyniki zapytań LINQ są zwykle zwracane do zmiennej typu var, która jest zmienną typowaną niejawnie.

Wyrażenia zapytań

  • Wyrażenia zapytań zawierają klauzulę from i mogą zawierać klauzule selectgroup byorder bywhere i join.
  • Złączenia join używają zawsze równoważności i dlatego używa się słowa kluczowego equals.
  • Wykonanie zapytania następuje w momencie jego wyliczenia. Można wymusić wykonanie zapytania używając funkcji agregującej.
  • Wiele operacji where jest równoważne z operatorem and.
  • Klauzula orderby jest używana do sortowania wyników, można oddzielić właściwości przecinkiem jeżeli chcemy sortować po więcej niż jednej.
  • W klauzuli select można utworzyć nowy typ dla wyników. Taka operacja nazywana jest projekcją.
  • Aby utworzyć złączenie zewnętrzne (left join) należy użyć klauzuli into i wywołać metodę DefaultIfEmpty aby ustawić wartości obiektu kiedy nie ma dopasowania w złączeniu.
  • Klauzula join może używać typów anonimowych aby połączyć sekwencje po więcej niż jednym polu.
  • Klauzula group by zwraca kolekcję typu IGrouping<TKey, TElement>.

Zapytania LINQ oparte o metody

  • Zapytania LINQ oparte o metody są równoważne z wyrażeniami zapytań.
  • Zapytania LINQ oparte o metody używają wyrażeń lambda.
  • Metoda SelectMany służy do spłaszczenia dwóch sekwencji w jedną.
  • Aby uzyskać złączenie zewnętrzne należy użyć metod GroupJoin i DefaultIfEmpty.
  • Dwie sekwencje można złączy za pomocą metody Concat.
  • Metoda Skip pomija określoną liczbę wierszy wyniku zapytania.
  • Metoda Take pobiera określoną liczbę wierszy wyniku zapytania.
  • Metoda Distinct pobiera niepowtarzające się elementy sekwencji.

LINQ to XML

  • Klasa XElement pozwala na zwrócenie wyniku zapytania LINQ jako dokument XML.