Zagadnienia egzaminu 70-483 opisane w tej notatce:

  • Operacje I/O - zapis i odczyt z plików oraz strumienie.
  • Konsumpcja danych - pobieranie, dodawanie, aktualizacja i usuwanie danych z bazy danych. Użycie ADO.NETADO.NET Entity Framework i WCF Data Services.
  • Serializacja - serializacja i deserializacja danych używając serializacji binarnej, XML, JSON, Data Contract i niestandardowej.
  • Kolekcje danych - przechowywanie i odczyt danych w kolekcjach i tablicach.

Praca z kolekcjami

Zrozumienie jak manipulować zbiorami danych jest kluczową umiejętnością dla każdego programisty. 
Tablice są najbardziej prymitywnymi typami w C# służącymi do przechowywania zbiorów danych, posiadają one oraniczoną funkcjonalność. Kolekcje są natomiast terminem ogólnym który obejmuje listy, słowniki, kolejki i inne obiekty.

Tablice

Tablica jest najbardziej podstawowym typem używanym do przechowywania zbiorów danych. Tablice posiadają elementy do których odnosimy się za pomocą indeksu używając nawiasów kwadratowych []
Poniższy kod tworzy jednowymiarową tablicę liczb całkowitych:

int[] mySet = newint[5];

mySet[0] = 1;
mySet[1] = 2;
mySet[2] = 3;
mySet[3] = 4;
mySet[4] = 5;

Tworząc tablicę trzeba określić liczbę elementów które tablica może posiadać. W przykładzie liczba elementów to 5. 
Tablice mogą być również wielowymiarowe. Przykład tworzenia tablicy dwuwymiarowej:

int[,] mySet = newint[3, 2];

mySet[0, 0] = 1;
mySet[0, 1] = 2;
mySet[1, 0] = 3;
mySet[1, 1] = 4;
mySet[2, 0] = 5;
mySet[2, 1] = 6;

Tablice mogą posiadać do 2,147,483,647 wymiarów.

Wszystkie tablice dziedziczą z klasy bazowej System.Array. Klasa ta posiada właściwości i metody użyteczne przy pracy z tablicami. Dwie najbardziej przydatne właściwości tablic to Length i Rank. Właściwość Length wskazuje całkowitą liczbę elementów we wszystkich wymiarach tablicy. Właściwość Rank wskazuje liczbę wymiarów tablicy.

Metoda Clone jest używana do wykonywania płytkiej kopii (ang. shallow copy) tablicy. Metoda CopyTo natomiast kopiuje elementy tablicy do innej tablicy.

Płytka kopia klonuje referencje do oryginalnych elementów tablicy więc dla typów referencyjnych są to te same elementy, np:

Person[] orginal = new Person[1];

orginal[0] = new Person() { Name = "John" };

Person[] clone = (Person[])orginal.Clone();

clone[0].Name = "Mary";

Debug.WriteLine("Original name " + orginal[0].Name);
Debug.WriteLine("Clone name " + clone[0].Name);

Wynik działania:

Original name Mary 
Clone name Mary

Aby zmienić element tylko w sklonowanej tablicy należy zastąpić go innym a nie zmieniać jego wartość, np:

Person[] orginal = new Person[1];

orginal[0] = new Person() { Name = "John" };

Person[] clone = (Person[])orginal.Clone();

clone[0] = new Person() { Name = "Bob" };

Debug.WriteLine("Original name " + orginal[0].Name);
Debug.WriteLine("Clone name " + clone[0].Name);

Wynik kodu:

Original name John 
Clone name Bob

Kolekcje

Kolekcje to ogólny termin dla specjalnych klas które są bardziej elastyczne niż tablice. Te klasy pozwalają na dynamiczne dodawanie i usuwanie elementów po ich zainicjowaniu, łączenie elementów kluczami, automatyczne sortowanie elementów i dodawanie elementów różnych typów. 
Najczęściej wykorzystywane kolekcje to ListListDictionaryDictionaryStack i Queue
Klasy kolekcji znajdują się w przestrzeniach nazw System.CollectionsSystem.Collections.Generic i System.Collections.Concurrent. Klasy zawarte w przestrzeni nazw System.Collections.Concurrent służą do wykonywania operacji wielowątkowych i nie będą opisane w tej notatce.

System.Collections

Przestrzeń nazw System.Collections zawiera klasy używane kiedy w kolekcji mamy dane różnego typu. Te kolekcje mogą być miksem typów wartości, klas lub struktur. 
Lista klas przestrzeni nazw System.Collections:

  • ArrayList - tworzy kolekcję której wielkość jest dynamiczna i może posiadac elementy różnego typu.
  • HashTable - tworzy kolekcję par klucz/wartość której wielkość jest dynamiczna i może posiadac elementy różnego typu.
  • Queue - tworzy kolekcję first-in-first-out.
  • SortedList - tworzy kolekcję par klucz/wartość której elementy są posortowane.
  • Stack - tworzy kolekcję last-in-first-out.

ArrayList

Klasa ArrayList pozwala na dynamiczne dodawanie elementów do kolekcji. Klasa ArrayList jest użyteczna kiedy nie znamy liczby elementów w momencie inicjalizacji kolekcji oraz kiedy chcemy przechowywać w kolekcji dane różnego typu. Metoda Add klasy ArrayList pobiera jako parametr typ Object i pozwala w ten sposób na przechowywanie dowolnego typu. 
Poniższy kod tworzy obiekt ArrayList i dodaje do niego elementy trzech różnych typów:

ArrayList myList = new ArrayList();

myList.Add(1);
myList.Add("hello world");
myList.Add(new DateTime(2012, 01, 01));

Lista najczęściej wykorzystywanych właściwości klasy ArrayList:

  • Capacity - pobiera lub ustawia liczbę elementów w kolekcji.
  • Count - pobiera aktualna liczbę elementów w kolekcji.
  • Item - pobiera lub ustawia element określony indeksem.

Lista najczęściej wykorzystywanych metod klasy ArrayList:

  • Add - dodaje element na końcu kolekcji.
  • AddRange - dodaje wiele elementów na końcu kolekcji.
  • BinarySearch - przeszukuje kolekcję używając domyślnej klasy porównywania i zwraca indeks elementu.
  • Clear - usuwa wszystkie elementy kolekcji.
  • Contains - sprawdza czy element należy do kolekcji.
  • CopyTo - kopiuje kolekcję do kompatybilnej tablicy jednowymiarowej.
  • IndexOf - przeszukuje kolekcje i zwraca indeks pierwszego wystąpienia elementu.
  • Insert - dodaje element na wyznaczonym indeksem miejscu w kolekcji.
  • Remove - usuwa element z kolekcji.
  • RemoveAt - usuwa element wyznaczony indeksem z kolekcji.
  • Reverse - odwraca kolejność elementów w kolekcji.
  • Sort - sortuje elementy w kolekcji.

Przykład sortowania dla elementów typu wartości:

ArrayList myList = new ArrayList();
myList.Add(4);
myList.Add(1);
myList.Add(5);
myList.Add(3);
myList.Add(2);
myList.Sort();

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

Wynik działania kodu:





5

Co jeżeli chcemy przechowywać obiekty które nie są standardowymi typami w kolekcji? Przykładowo jeżeli utworzymy klasę z właściwością ID która przechowuje identyfikator obiektu, np:

classMyObject
{
publicint ID{ get; set; }
}

Teraz utworzymy kolekcję z pięcioma instancjami naszej klasy i wywołamy metodę Sort:

ArrayList myList = new ArrayList();

myList.Add(new MyObject() { ID = 4 });
myList.Add(new MyObject() { ID = 1 });
myList.Add(new MyObject() { ID = 5 });
myList.Add(new MyObject() { ID = 3 });
myList.Add(new MyObject() { ID = 2 });

myList.Sort();

foreach (MyObject i in myList)
{
Console.WriteLine(i.ID.ToString());
}

Jeżeli uruchomimy ten kod to w rezultacie otrzymamy wyjątek wyrzucony w metodzie SortFailed to Compare Two Elements in the Array. Dzieje się tak ponieważ metoda nie wie jak wykonac sortowanie dla naszej klasy. Aby to naprawić należy zaimplementować interfejs IComparable w utworzonej klasie i zaimplementować metodę CompareTo służącą do porównywania wartości elementów. Metoda ta pobiera jeden argument - obiekt który chcemy porównać i w wyniku zwraca liczbę mniejszą od zera jeżeli aktualna instancja jest wyższa w kolejności sortowania, zero jeżeli aktualna instancja jest równa lub liczbę większą od zera jeżeli aktualna instancja jest mniejsza w kolejności sortowania.

classMyObject : IComparable
{
publicint ID{ get; set; }

publicintCompareTo(object obj)
{
MyObject obj1 = obj as MyObject;
returnthis.ID.CompareTo(obj1.ID);
}
}

Innym powszechnym zastosowaniem tablic i kolekcji jest możliwość przeszukiwania tablicy. Można do tego celu uzyć pętli for lub foreach albo użyć dużo szybszej metody jaką jest BinarySearch. Aby użyć metody BinarySearch elementy kolekcji musza być posortowane. MetodaBinarySearch zwraca indeks elementu który odnalazła. Jeżeli nic nie znalazła to wynikiem będzie liczba ujemna, np:

ArrayList myList = new ArrayList();

myList.Add(new MyObject() { ID = 4 });
myList.Add(new MyObject() { ID = 1 });
myList.Add(new MyObject() { ID = 5 });
myList.Add(new MyObject() { ID = 3 });
myList.Add(new MyObject() { ID = 2 });

myList.Sort();

int foundIndex = myList.BinarySearch(new MyObject() { ID = 4 });

if (foundIndex >= 0)
{
Debug.WriteLine(((MyObject)myList[foundIndex]).ID.ToString());
}
else
{
Debug.WriteLine("Element not found");
}

Hashtable

Kolekcja Hashtable pozwala na przechowywanie par klucz/wartość dowolnego typu. Dane są zapisywane zgodnie z hashcode klucza i mogą być dostępne za pomocą klucza a nie indeksu elementu. 
Poniższy kod tworzy kolekcję Hashtable i wstawia do niej elementy różnego typu:

Hashtable myHashtable = new Hashtable();

myHashtable.Add(1, "one");
myHashtable.Add("two", 2);
myHashtable.Add(3, "three");

Debug.WriteLine(myHashtable[1].ToString());
Debug.WriteLine(myHashtable["two"].ToString());
Debug.WriteLine(myHashtable[3].ToString());

Wynik działania kodu:

one 

three

Queue

Queue jest kolekcją typu first-in-first-out. Kolekcja Queue może być użyteczna, gdy trzeba przechowywać dane w określonej kolejności, do sekwencyjnego przetwarzania. 
Poniższy kod tworzy kolekcję Queue i dodaje do niej trzy elementy, usuwa wszystkie elementy i wyświetla ich wartość:

Queue myQueue = new Queue();

myQueue.Enqueue("first");
myQueue.Enqueue("second");
myQueue.Enqueue("third");

intcount = myQueue.Count;
for (int i = 0; i < count; i++)
{
Debug.WriteLine(myQueue.Dequeue());
}

Wynik działania:

first 
second 
third

Zamiast metody Add kolekcja Queue posiada metodę Enqueue, która dodaje element do kolejki. Do usuwania elementu używana jest metoda Dequeue. Nie można dostać się do elementów kolejki ani za pomocą klucza ani indeksu. Zamiast tego można tylko dodać, usunąć lub podejrzeć wartość na sczycie kolejki. Metoda Peek zwraca wartość elementu na szczycie kolejki ale go nie usuwa.

SortedList

Kolekcja SortedList zawiera pary klucz/wartość ale jest inna niż Hashtable ponieważ do elementu możemy odnieść się za pomocą klucza i indeksu, oraz dlatego, że jest posortowana. Elementy kolekcji SortedList są sortowane używając implementacji IComparable dla klucza lub implementacji IComparer kiedy lista jest tworzona. 
Poniższy kod tworzy kolekcję SortedList i dodaje do niej trzy elementy oraz wyświetla je:

SortedList mySortedList = new SortedList();

mySortedList.Add(3, "three");
mySortedList.Add(2, "second");
mySortedList.Add(1, "first");

foreach (DictionaryEntry item in mySortedList)
{
Debug.WriteLine(item.Value);
}

Wynik działania kodu:

first 
second 
third

Do kolekcji SortedList można dodawać tylko elementy których klucze mogą być ze sobą porównywane.

Stack

Stack jest kolekcją typu last-in-first-out. Kolekcja Stack może być użyteczna, gdy trzeba przechowywać dane w określonej kolejności, do sekwencyjnego przetwarzania. Zamiast metody Add kolekcja Stack posiada metodę Push. Do usuwania elementu używana jest metoda Pop. MetodaPeek zwraca wartość elementu na szczycie kolejki ale go nie usuwa. 
Poniższy kod tworzy kolekcję Stack i dodaje do niej trzy elementy, usuwa wszystkie elementy i wyświetla ich wartość:

Stack myStack = new Stack();

myStack.Push("first");
myStack.Push("second");
myStack.Push("third");

intcount = myStack.Count;
for (int i = 0; i < count; i++)
{
Debug.WriteLine(myStack.Pop());
}

Wynik działania kodu:

third 
second 
first

System.Collections.Generic

Przestrzeń nazw System.Collections.Generic zawiera klasy używane kiedy znamy typ danych które będziemy przechowywali w kolekcji i chcemy aby cała kolekcja zawierała elementy tylko jednego typu. 
Kolekcje w przestrzeni nazw System.Collections.Generic:

  • Dictionary<TKey, TValue> - kolekcja par klucz/wartość tego samego typu.
  • List< T > - kolekcja obiektów tego samego typu.
  • Queue< T > - kolekcja first-in-first-out z elementami tego samego typu.
  • SortedList<TKey, TValue> - kolekcja par klucz/wartość tego samego typu które są sortowane wg klucza.
  • Stack< T > - kolekcja last-in-first-out z elementami tego samego typu.

Kolekcje generyczne są bardziej wydajne i powinno się ich używać jeżeli to możliwe.

Dictionary<TKey, TValue>

Kolekcja Dictionary<TKey, TValue> pozwala na przechowywanie zbioru elementów, gdzie każdy element jest skojarzony kluczem. Zamiast indeksu do pobrania elementu z Dictionary używa się klucza. Może to być przydatne np do przechowywania elementów tabeli bazy danych gdzie kluczem może zostać identyfikator. 
Poniższy kod tworzy klasę MyRecord reprezentującą rekord w tabeli, która ma trzy kolumny. Kolekcja Dictionary jest użyta do przechowania wielu elementów tej klasy:

class MyRecord
{
publicint ID { get; set; }
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
}

staticvoidSample1()
{
Dictionary<int, MyRecord> myDictionary = new Dictionary<int, MyRecord>();

myDictionary.Add(5, new MyRecord() { ID = 5,
FirstName = "Bob",
LastName = "Smith" });

myDictionary.Add(2, new MyRecord() { ID = 2,
FirstName = "Jane",
LastName = "Doe" });

myDictionary.Add(10, new MyRecord() { ID = 10,
FirstName = "Bill",
LastName = "Jones" });

Debug.WriteLine(myDictionary[5].FirstName);
Debug.WriteLine(myDictionary[2].FirstName);
Debug.WriteLine(myDictionary[10].FirstName);
}

Wynik działania kodu:

Bob 
Jane 
Bill

Liczbę elementów kolekcji Dictionary uzyskujemy używając właściwości Count.

Najczęściej używane metody kolekcji Dictionary:

  • Add - dodaje klucz i wartość do słownika.
  • Clear - usuwa wszystkie elementy kolekcji.
  • ContainsKey - zwraca true jeżeli kolekcja zawiera określony klucz.
  • ContainsValue - zwraca true jeżeli kolekcja zawiera określoną wartość.
  • Remove - usuwa element określony kluczem.

Jeżeli odwołujemy się do elementy Dictionary wg klucza który nie istnieje to wyrzucony zostaje wyjątek. Dobrą praktyką jest używanie metody ContainsKey zanim pobierzemy element wg klucza.

List< T >

Kolekcja List jest silnie typowaną kolekcją obiektów. Jest podobna do ArrayList z wyjątkiem tego, że wszystkie elementy musza posiadać ten sam typ.

Poniższy kod tworzy listę liczb całkowitych i dodaje do niej trzy elementy:

List<int> myList = new List<int>();

myList.Add(1);
myList.Add(2);
myList.Add(3);

Kolekcje SortedList<TKey, TValue>Queue< T > i Stack< T > są silnie typowanymi odpowiednikami kolekcji z przestrzeni nazw System.Collections.

Kolekcje niestandardowe

Oprócz standardowych kolekcji dostarczonych przez .NET, można tworzyć własne silnie typowane kolekcje. Silnie typowane kolekcje są użyteczne ponieważ nie ponoszą strat wydajnościowych spowodowanych przez boxing i unboxing. Aby utworzyć własną kolekcję należy dziedziczyć z klasy bazowej CollectionBase
Najczęściej używane właściwości klasy System.Collections.CollectionBase:

  • Capacity - pobiera lub ustawia liczbę elementów jaką może posiadać kolekcja.
  • Count - zwraca liczbę elementów kolekcji.
  • InnerList - zwraca kolekcję ArrayList elementów w kolekcji.
  • List - zwraca kolekcję IList elementów w kolekcji.

Najczęściej używane metody klasy System.Collections.CollectionBase:

  • Clear - czyści elementy z kolekcji.
  • OnInsert - umożliwia wykonanie niestandardowego przetwarzania przed wstawieniem nowego elementu.
  • OnRemove - umożliwia wykonanie niestandardowego przetwarzania przed usunięciem elementu.
  • OnSet - umożliwia wykonanie niestandardowego przetwarzania przed ustawieniem wartości elementu.
  • RemoveAt - usuwa element na pozycji określonej indeksem.

Przykładowo utworzymy kolekcję niestandardową zawierającą elementy typu Person. Klasa Person wygląda następująco:

classPerson
{
publicint PersonId { get; set; }
publicstring FName { get; set; }
publicstring LName { get; set; }
publicstring Address { get; set; }
publicstring City { get; set; }
publicstring State { get; set; }
publicstring ZipCode { get; set; }
}

Utwórzmy teraz kolekcję niestandardową dziedziczącą z klasy CollectionBase. Poniższa kolekcja będzie posiadała metody AddInsert i Remove oraz będzie posiadała silnie typowany indexer. Indexer jest używany kiedy chcemy odnieść się do elementów kolekcji używając indeksu:

classPersonCollection : CollectionBase
{
publicvoidAdd(Person person)
{
List.Add(person);
}

publicvoidInsert(int index, Person person)
{
List.Insert(index, person);
}

publicvoidRemove(Person person)
{
List.Remove(person);
}

public Person this[int index]
{
get
{
return (Person)List[index];
}
set
{
List[index] = value;
}
}
}

Przykład użycia niestandardowej kolekcji PersonCollection:

staticvoidMain(string[] args)
{
PersonCollection persons = new PersonCollection();

persons.Add(new Person() {
PersonId = 1,
FName = "John",
LName = "Smith" });

persons.Add(new Person()
{
PersonId = 2,
FName = "Jane",
LName = "Doe" });

persons.Add(new Person()
{
PersonId = 3,
FName = "Bill Jones",
LName = "Smith" });

foreach (Person person in persons)
{
Debug.WriteLine(person.FName);
}
}

Konsumowanie danych

Sekcja ta opisuje jak pobierać dane z bazy danych za pomocą ADO.NETEntity Framework i WCF Data Service.

Praca z ADO.NET

ADO.NET jest zestawem klas w .NET Framework które pozwalaja na połączenie z bazą danych oraz wykonanie na niej operacji. 
Klasy ADO.NET są zlokalizowane w przestrzeni nazw System.Data. W przestrzeni te znajduje się wiele klas bazowych i interfejsów które trzeba zaimplementować aby połączyć się z odpowiednim dostawcą danych. Przykładowo przestrzeń nazw System.Data.SqlClient zawiera zestaw klas i interfejsów do połączenia z SQL Server.

Connection

Obiekt Connection jest używany do uzyskania połączenia z bazą danych. Klasa SqlConnection jest używana do połączenia z bazą danych SQL Server. Każda dostarczane przez różnych dostawców muszą dziedziczyć z klasy bazowej Data.Common.DBConnection.

Najczęściej używane właściwości klasy DBConnection:

  • ConnectionString - pobiera lub ustawia ciąg znaków używany do połączenia z bazą danych.
  • ConnectionTimeout - pobiera czas w sekundach, który system powinien czekać podczas nawiązywania połączenia z bazą danych przed wygenerowaniem błędu.
  • Database - pobiera nazwę bazy danych.
  • DataSource - pobiera nazwę serwera.
  • ServerVersion - pobiera wersję serwera dla bazy danych.
  • State - pobiera ciąg znaków reprezentujący stan połączenia z bazą danych np Open lub Closed.

Najważniejszą właściwością jest ConnectionString. Dla bazy danych SQL Server przyjmuje on następującą formę:

Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;

Najczęściej używane metody klasy DBConnection:

  • BeginTransaction - rozpoczyna transakcję w bazie danych.
  • Close - zamyka połączenie z bazą danych.
  • GetSchema - zwraca obiekt DataTable zawierający informacje o schemacie.
  • Open - otwiera połączenie z bazą danych używając ciągu ConnectionString.

Metoda Open otwiera połączenie z bazą danych. Po uzyskaniu połączenia możemy używać innych obiektów ADO.NET służących do wykonywania operacji na bazie danych. 
Poniższy kod tworzy połączenie z bazą danych SQL Server:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;";
cn.Open();

Command

Obiekt Command jest używany do wykonywania operacji na bazie danych. Za pomocą Command można wykonać selectinsertupdatedelete lub procedurę składowaną. Klasa System.Data.Common.DBCommand jest klasą bazową dla wszystkich klas dostawców. KlasaSystem.Data.SqlClient.SqlCommand jest implementacją Command dla bazy danych SQL Server.

Metoda ExecuteNonQuery

Metoda ExecuteNonQuery jest używana do wykonywania wyrażenia na bazie danych które nie zwraca wyniku. Przykładowo instrukcja insertupdate lub delete
Poniższy kod demonstruje użycie instrukcji insert na tabeli bazy danych:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;";
cn.Open();

SqlCommand cmd = new SqlCommand();
cmd.Connection = cn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = "INSERT INTO Person (FirstName, LastName) " +
"VALUES ('Joe', 'Smith')";
cmd.ExecuteNonQuery();

cn.Close();

Przed wykonaniem metody ExecuteNonQuery należy ustawić trzy właściwości obiektu SqlCommand. Pierwszą jest właściwość Connectiod która musi być ustawiona na otwarte połączenie z bazą danych. Właściwość CommandType posiada w tym przykładzie wartośćCommandType.Text ponieważ używamy instrukcji w języku SQL. Właściwość CommandText zawiera kod SQL który chcemy wykonać.

Jeżeli chcemy wykonać procedurę składowaną to należy ustawić właściwość CommandType na CommandType.StoredProcedure oraz ustawić właściwość CommandText na nazwę procedury.

Przykładowo utwórzmy procedurę składowaną w bazie danych w języku T-SQL:

CREATEPROCEDUREPersonInsert
@FirstNamevarchar(50),
@LastNamevarchar(50)
AS
BEGIN
INSERTINTOPERSON(FirstName, LastName)VALUES(@FirstName, @LastName)
END

Poniższy kod wykonuje procedurę składowaną i wstawia do niej parametry:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;";
cn.Open();

SqlCommand cmd = new SqlCommand();
cmd.Connection = cn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "PersonInsert";
cmd.Parameters.Add(new SqlParameter("@FirstName", "Joe"));
cmd.Parameters.Add(new SqlParameter("@LastName", "Smith"));
cmd.ExecuteNonQuery();

cn.Close();

Obiekt Command posiada właściwość Parameters która jest używana do wstawiania parametrów do procedur składowanych. 
Metoda ExecuteNonQuery zwraca w wyniku liczbę wierszy które zostały zmienione przez wykonanie zapytania.

Metoda ExecuteReader

Metody ExecuteReader używa się aby pobrać dane z bazy danych np za pomocą instrukcji select. Metoda ExecuteReader zwraca obiekt DBDataReader. Klasa DBDataReader jest zdefiniowana w ADO.NET i służy do pobierania wyników podczas gdy obiekt jest połączony z bazą danych. Obiekt DBDataReader jest obiektem typu forward-only co oznacza, że możemy przechodzić przez rekord wynikowy raz i nie możemy cofnąć kursora do poprzedniej pozycji.

Poniższy kod wyświetla wszystkie rekordy tabeli Person za pomocą obiektu SqlDataReader który jest implementacją DBDataReader dla bazy SQL Server:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;"
;
cn.Open();

SqlCommand cmd = new SqlCommand();
cmd.Connection = cn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT * FROM Person";
SqlDataReader dr = cmd.ExecuteReader();

if (dr.HasRows)
{
while (dr.Read())
{
Debug.WriteLine(string.Format("First Name: {0} , Last Name: {1}",
dr["FirstName"], dr["LastName"]));
}
}
dr.Close();
cn.Close();

Po zakończeniu odczytu należy zamknąć obiekt DBDataReader a następnie obiekt Connection
Metoda ExecuteReader posiada przeciążenie z parametrem typu CommandBehavior pozwalające na automatyczne zamykanie połączenia po zakończeniu odczytu.

Drugą metodą zamykania połączenia w C# jest użycie bloku using. Jest to zalecana metoda używania połączeń. 
Składnia użycia using z obiektem Connection wygląda następująco:

using (SqlConnection cn = new SqlConnection())
{
}

Najczęściej wykorzystywane właściwości klasy DBDataReader:

  • FieldCount - zwraca liczbę kolumn aktualnego wiersza.
  • HasRows - zwraca true jeżeli DBDataReader posiada jakiekolwiek wiersze.
  • IsClosed - zwraca true jeżeli DBDataReader jest zamknięty.
  • Item[Int32] - jest to indekser zwracający kolumnę bazując na indeksie.
  • Item[String] - jest to indekser zwracający kolumnę bazując na jej nazwie.

Nie istnieje właściwość Count dla obiektu DBDataReader. Aby dowiedzieć się ile jest wierszy w wyniku należy przejść przez wszystkie wiersze obiektu DBDataReader.

Ważną rzeczą jest zapamiętanie, że dla kolumny zawierającej null obiekt DBDataReader zwraca wartość typu DBNull.Value a nie null.

Najczęściej wykorzystywane metody klasy DBDataReader:

  • Close - zamyka obiekt DBDataReader.
  • GetBoolean - zwraca wynik wybranej kolumny jako typ bool.
  • GetByte - zwraca wynik wybranej kolumny jako typ byte.
  • GetChar - zwraca wynik wybranej kolumny jako typ char.
  • GetDateTime - zwraca wynik wybranej kolumny jako typ DateTime.
  • GetDecimal - zwraca wynik wybranej kolumny jako typ decimal.
  • GetDouble - zwraca wynik wybranej kolumny jako typ double.
  • GetFieldType - zwraca typ danych wybranej kolumny.
  • GetFieldValue - zwraca wartość wybranej kolumny jako typ T.
  • GetFloat - zwraca wynik wybranej kolumny jako typ float.
  • GetGuid - zwraca wynik wybranej kolumny jako typ Guid.
  • GetInt16 - zwraca wynik wybranej kolumny jako typ Int16.
  • GetInt32 - zwraca wynik wybranej kolumny jako typ Int32.
  • GetInt64 - zwraca wynik wybranej kolumny jako typ Int64.
  • GetName - zwraca nazwę wybranej kolumny.
  • GetOrdinal - zwraca pozycję wybranej kolumny.
  • GetSchemaTable - zwraca obiekt DataTable zawierający metadane kolumn.
  • GetString - zwraca wynik wybranej kolumny jako typ string.
  • GetValue - zwraca wynik wybranej kolumny jako typ object.
  • GetValues - wypełnia tablicę obiektów wartościami kolumn.
  • NextResult - przenosi kursor do następnego wyniku w obiekcie DBDataReader.
  • IsDBNull - sprawdza czy wynik zawiera wartość null.
  • Read - przesuwa kursor do następnego rekordu jeżeli rekord istnieje.

Dobrą praktyką jest sprawdzenie właściwości HasRows przed użyciem metody Read ponieważ metoda Read zgłosi wyjątek jeżeli nie będzie następnego rekordu w obiekcie DBDataReader.

Pobierając dane z bazy danych można wykonać kilka instrukcji Select w jednym wywołaniu. Klasa DBDataReader potrafi przechowywać wiele wyników w jednym obiekcie. Aby przenieść się do następnego wyniku używamy metody NextResult.

Metoda GetSchemaTable zwraca obiekt DataTable zawierający metadane kolumn w obiekcie DBDataReader. Zwrócona tabela posiada wiersz dla każdej kolumny i zawiera nazwę kolumny, typ, rozmiar, pozycję porządkową, informację czy jest to kolumna Identity oraz czy zezwala na wartości null. Jeżeli chcemy pobrać tylko metadane dla tabeli to możemy wykonać metodę ExecuteDataReader z parametrem CommandBehavior.SchemaOnly.

Metoda ExecuteScalar

Metody ExecuteScalar używamy kiedy wiemy, że wynik zapytania do bazy danych zwróci tylko jeden wynik tzn jedną kolumnę z jednym wierszem. 
Poniższy kod wywołuje metodę ExecuteScalar i zwraca liczbę wierszy w tabeli Person:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;";
cn.Open();

SqlCommand cmd = new SqlCommand();
cmd.Connection = cn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT COUNT(*) FROM Person";
object obj = cmd.ExecuteScalar();

Debug.WriteLine(string.Format("Count: {0}", obj.ToString()));

cn.Close();

Metoda ExecuteScalar zawsze zwraca wynik typu object który możemy rzutować do odpowiedniego typu wyniku.

Metoda ExecuteXmlReader

Metoda ExecuteXmlReader zwraca XmlReader, który pozwala zaprezentować dane jako XML
Poniższy kod zwraca dane z tabeli Person w postaci XmlReader:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;"
;
cn.Open();

SqlCommand cmd = new SqlCommand();
cmd.Connection = cn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT * FROM Person FOR XML AUTO, XMLDATA";
System.Xml.XmlReader xml = cmd.ExecuteXmlReader();

cn.Close();

Wynik zapytania:

<Schema name="Schema1" xmlns="urn:schemas-microsoft-com:xml-data"
xmlns:dt="urn:schemas-microsoft-com:datatypes">
<ElementType name="Person" content="empty" model="closed">
<AttributeType name="PersonId" dt:type="i4"/>
<AttributeType name="FirstName" dt:type="string"/>
<AttributeType name="LastName" dt:type="string"/>
<AttributeType name="Address" dt:type="string"/>
<AttributeType name="City" dt:type="string"/>
<AttributeType name="State" dt:type="string"/>
<AttributeType name="ZipCode" dt:type="string"/>
<attribute type="PersonId"/>
<attribute type="FirstName"/>
<attribute type="LastName"/>
<attribute type="Address"/>
<attribute type="City"/>
<attribute type="State"/>
<attribute type="ZipCode"/>
</ElementType>
</Schema>
<Person xmlns="x-schema:#Schema1" PersonId="1" FirstName="John" LastName="Smith"
Address="123 First Street" City="Philadelphia" State="PA" ZipCode="19111"/>

DataSet, DataTable i DataAdapter

Innym sposobem na pobranie danych z bazy danych jest użycie DataSet i DataTable. Możliwości klasy DataTable są podobne do DBDataReader , poza tym , że DataTable jest odłączona od bazy danych, można poruszać się kursorem w przód i w tył oraz można aktualizować dane, połączyć się ponownie do bazy danych i zapisać zmiany. Klasa DataSet jest kontenerem dla jednego lub wielu DataTable. Można wykonać instrukcję SQL która zwróci wiele wyników i każdy z nich będzie zawarty w DataSet. Można wtedy filtrować, sortować i aktualizować te dane w pamięci. Klasa DataAdapter służy do wypełnienia DataSet lub DataTable oraz ponownego połączenia z bazą danych aby wykonać komendę insertupdate lub delete
Poniższy kod używa DataSet do pobrania danych z tabeli Person i ich wyświetlenia:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;"
;
cn.Open();

SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Person", cn);
DataSet ds = new DataSet();
da.Fill(ds, "Person");

cn.Close();

foreach (DataRow row in ds.Tables[0].Rows)
{
Debug.WriteLine(string.Format("First Name: {0} , Last Name: {1}",
row
["FirstName"], row["LastName"]));
}

Metoda Fill klasy SqlDataAdapter służy do wypełnienia DataSet
Klasa DataSet posiada właściwość Tables której można uzyć aby odwołać się do obiektów DataTable które zostały zwrócone przez zapytanie. W powyższym przykładzie powstał tylko jeden obiekt DataTable więc można się do niego odwołać poprzez indeks o wartości zero. 
Klasa DataTable posiada właściwość Rows, zawierającą kolekcję obiektów DataRow które reprezentują zwrócone rekordy. Do rekordów można odwołać się poprzez indeks lub iterując kolekcję. Kolekcja Rows posiada właściwość Count.

Klasa DataAdapter pozwala na wstawianie, aktualizowanie i usuwanie wierszy po zmianie obiektu DataTable. Poniższy przykład pokazuje jak użyć obiektu DataAdapter do wstawiania rekordów do bazy danych:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;";
cn.Open();

SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Person", cn);

//Create the insertcommand
SqlCommand insert = new SqlCommand();
insert.Connection = cn;
insert.CommandType = CommandType.Text;
insert.CommandText = "INSERT INTO Person (FirstName, LastName) VALUES (@FirstName,
@LastName)";

//Create the parameters
insert.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.VarChar, 50,
"FirstName"));
insert.Parameters.Add(new SqlParameter("@LastName", SqlDbType.VarChar, 50,
"LastName"));

//Associate the insertcommand with the DataAdapter.
da.InsertCommand = insert;

//Get the data.
DataSet ds = new DataSet();
da.Fill(ds, "Person");

//Add anew row.
DataRow newRow = ds.Tables[0].NewRow();
newRow["FirstName"] = "Jane";
newRow["LastName"] = "Doe";
ds.Tables[0].Rows.Add(newRow);

//Update the database.
da.Update(ds.Tables[0]);

cn.Close();

Poniższy kod demonstruje jak użyć obiektu DataAdapter do aktualizacji i usuwania rekordów bazy danych:

SqlConnection cn = new SqlConnection();
cn.ConnectionString = "Server=myServerAddress;Database=myDataBase;
User Id=myUsername;Password=myPassword;";
cn.Open();

SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Person", cn);

//Create the updatecommand
SqlCommand update = new SqlCommand();
update.Connection = cn;
update.CommandType = CommandType.Text;
update.CommandText = "UPDATE Person SET FirstName = @FirstName, LastName = @LastName
WHERE PersonId = @PersonId";

//Create the parameters
update.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.VarChar, 50,
"FirstName"));
update.Parameters.Add(new SqlParameter("@LastName", SqlDbType.VarChar, 50,
"LastName"));
update.Parameters.Add(new SqlParameter("@PersonId", SqlDbType.Int, 0, "PersonId"));

//Create the deletecommand
SqlCommand delete = new SqlCommand();
delete.Connection = cn;
delete.CommandType = CommandType.Text;
delete.CommandText = "DELETE FROM Person WHERE PersonId = @PersonId";

//Create the parameters
SqlParameter deleteParameter = new SqlParameter("@PersonId", SqlDbType.Int, 0,
"PersonId");
deleteParameter.SourceVersion = DataRowVersion.Original;
delete.Parameters.Add(deleteParameter);

//Associate the updateanddelete commands with the DataAdapter.
da.UpdateCommand = update;
da.DeleteCommand = delete;

//Get the data.
DataSet ds = new DataSet();
da.Fill(ds, "Person");

//Update the first row
ds.Tables[0].Rows[0]["FirstName"] = "Jack";
ds.Tables[0].Rows[0]["LastName"] = "Johnson";

//Delete the second row.
ds.Tables[0].Rows[1].Delete();

//Updat the database.
da.Update(ds.Tables[0]);

cn.Close();

Praca z Entity Framework

Biblioteka ADO.NET Entity Framework jest zestawem klas który również umożliwia wykonywanie operacji na bazie danych. Ponadto Entity Framework posiada interfejs graficzny pozwalający na przeciąganie i upuszczanie obiektów bazy danych na powierzchnię projektową. BibliotekaADO.NET Entity Framework jest narzędziem zwanym ORM (ang. Object-Relational Mapping). 
Przykłady tej sekcji będą używały bazy danych Northwind dostępnej pod adresem http://northwinddatabase.codeplex.com/.

Tworzenie modelu Entity Framework w Visual Studio

Rdzeniem pracy z Entity Framework jest model. Model zawiera wszystkie klasy które reprezentują obiekty w bazie danych. 
Aby utworzyć model bazy danych Northwind należy:

  1. Uruchomić Visual Studio.
  2. Utworzyć nowy projekt np typu Console Application i nadać mu nazwę np NorthwindsConsole.
  3. Po utworzeniu dodać nowy element z menu rozwijanego w oknie Solution Explorer.
  4. Dodać ADO.NET Entity Data Model z listy zainstalowanych szablonów i nadać mu nazwę np NorthwindsModel.
  5. Ponieważ baza danych już istnieje wybieramy Generate from Database. Takie podejście tworzenia modelu nazywane jest Database First. Alternatywnym podejściem jest Model First, które pozwala na wygenerowanie bazy danych po utworzeniu klas modelu danych.
  6. W oknie połączenia z bazą danych należy wybrać nowe połączenie z danymi łączącymi z bazą danych Northwind.
  7. Po wybraniu połączenia należy wybrać obiekty które mają być zmapowane w modelu. Wybieramy tabele, widoki i procedury składowane.

Generator Entity Framework wygeneruje model który będzie wyglądał tak: 
Alt text

Do projektu został dodany plik NorthwindsModel.edmx, jest to graficzna reprezentacja modelu. Klikając strzałkę przy nazwie modelu w oknie Solution Explorer możemy zobaczyć wszystkie klasy jakie zostały wygenerowane dla modelu.

Plik NorthwindsModel.tt jest plikiem Text Transformation Template Toolkit zwanym również szablonem T4. Szablony T4 są używane do automatycznego generowania kodu w Visual Studio. Klikając strzałkę obok pliku zobaczymy listę wygenerowanych przez model klas. jak widać dla każdej tabeli, widoku i procedury został wygenerowany osobny plik. 
Przykładowo plik Categories.cs jest plikiem klasy wygenerowanej dla tabeli Category i ma następującą postać:

publicpartialclassCategories
{
publicCategories()
{
this.Products = new HashSet<Products>();
}

publicint CategoryID { get; set; }
publicstring CategoryName { get; set; }
publicstring Description { get; set; }
publicbyte[] Picture { get; set; }

publicvirtual ICollection<Products> Products { get; set; }
}

Dla każdej kolumny została wygenerowana właściwość. Została również wygenerowana inna właściwość ICollection Products która jest wygenerowaną kolekcją dla tabeli połączonej kluczem obcym.

Plik NorthwindsModel.Context.tt jest szablonem T4 dla obiektu Context. Obiekt Context jest obiektem który reprezentuje połączenie z całą bazą danych. Jeżeli rozwiniemy plik to zobaczymy plik NorthwindsModel.Context.cs w którym zdefiniowana jest klasa NorthwindsEntitiesposiadająca właściwości dla każdej tabeli w bazie danych. Właściwości są typu generycznego DbSet, który jest kolekcją dla każdego typu reprezentującego tabelę lub widok np:

public DbSet<Category> Categories { get; set; }

Procedury składowane zostały stworzone jako metody o takiej samej nazwie. Parametry procedur są zmapowane na parametry tych metod. Jeżeli procedura zwraca wynik to metoda posiada typ zwracanej kolekcji ObjectResult. Przykładowo procedura CustOrderHist została zmapowana na następującą metodę:

publicvirtual ObjectResult<CustOrderHist_Result> CustOrderHist(string customerID)
{
var customerIDParameter = customerID != null ?
new ObjectParameter("CustomerID", customerID) :
new ObjectParameter("CustomerID", typeof(string));

return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction
<CustOrderHist_Result>("CustOrderHist", customerIDParameter);
}

Klasa CustOrderHist_Result jest typem utworzonym dla zwracanych wartości procedury. procedura posiada jeden parametr zmapowany jako customerID. Aby wywołać procedurę wykonywana jest metoda ExecuteFunction która jest zdefiniowana w klasie bazowej DbContext. KlasaDbContext jest zdefiniowana w przestrzeni nazw System.Data.Entity.

Pobieranie rekordów

Posiadając model możemy pobrać dane nie znając języka SQL i posługując się jedynie obiektami. 
Poniższy fragment kodu pobiera rekordy z tabeli Category i wyświetla je:

using (NORTHWNDEntities db = new NORTHWNDEntities())
{
var categories = from c in db.Categories select c;

foreach (Categories category in categories)
{
Debug.WriteLine(
string.Format("CategoryId: {0}, CategoryName: {1}",
category.CategoryID, category.CategoryName));
}
}

Wynik działania zapytania:

CategoryId: 1, CategoryName: Beverages 
CategoryId: 2, CategoryName: Condiments 
CategoryId: 3, CategoryName: Confections 
CategoryId: 4, CategoryName: Dairy Products 
CategoryId: 5, CategoryName: Grains/Cereals 
CategoryId: 6, CategoryName: Meat/Poultry 
CategoryId: 7, CategoryName: Produce 
CategoryId: 8, CategoryName: Seafood

Do pobierania danych z użyciem Entity Framework używane są zapytania LINQ.

Poniższy kod demonstruje pobieranie danych z wielu tabel połączonych kluczem obcym:

using (NORTHWNDEntities db = new NORTHWNDEntities())
{
var products = from c in db.Categories
join p in db.Products on c.CategoryID equals p.CategoryID
select p;

foreach (Products product in products)
{
Debug.WriteLine(
string.Format(
"ProductName: {0}, CategorName: {1}",
product.ProductName,
product.Categories.CategoryName));
}
}

Wstawianie rekordów

Poniższy fragment kodu demonstruje wstawianie rekordu do tabeli Category za pomocą Entity Framework:

using (NORTHWNDEntities db = new NORTHWNDEntities())
{
Categories category = new Categories() {
CategoryName = "Alcohol",
Description = "Happy Beverages"
};

db.Categories.Add(category);
db.SaveChanges();
}

Po utworzeniu instancji klasy Categories została ona dodana do kolekcji db.Categories za pomocą metody Add. Zmiany w bazie danych zostały zapisane za pomocą metody SaveChanges.

Aktualizacja rekordów

Poniższy fragment kodu demonstruje aktualizację rekordu z tabeli Category za pomocą Entity Framework:

using (NORTHWNDEntities db = new NORTHWNDEntities())
{
Categories category = db.Categories.First(c => c.CategoryName == "Alcohol");
category.Description = "Happy People";
db.SaveChanges();
}

Usuwanie rekordów

Poniższy fragment kodu demonstruje usuwanie rekordu z tabeli Category za pomocą Entity Framework:

using (NORTHWNDEntities db = new NORTHWNDEntities())
{
Categories category = db.Categories.First(c => c.CategoryName == "Alcohol");
db.Categories.Remove(category);
db.SaveChanges();
}

W wersjach wcześniejszych niż Entity Framework 5.0 usuwanie rekordów odbywało się z użyciem metody DeleteObject.

Wywołanie procedury składowanej

Ponieważ procedury składowane są wygenerowane przez EF jako metody to ich wywołanie oznacza proste wywołanie metody np:

using (NORTHWNDEntities db = new NORTHWNDEntities())
{
var custOrderHist = db.CustOrderHist("ALFKI");

foreach (CustOrderHist_Resultresultin custOrderHist)
{
Debug.WriteLine(
string.Format("ProductName: {0}, Total: {1}",
result.ProductName,
result.Total));
}
}

WCF Data Services

WCF Data Services są komponentami .NET Framework które umożliwiają dostęp do bazy danych poprzez użycie URI. W poprzednich wersjach usługa ta nazywała się ADO.NET Data Services. Używając URI i ich parametrów można pobierać, filtrować, aktualizować a nawet usuwać dane. WCF Data Services używają Open Data Protocol (OData), który jest protokołem sieciowym używającym HTTP
Przykładowo poniższy adres może być użyty do pobrania przefiltrowanych kategorii z bazy danych Northwind:

http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/Categories?$filter=CategoryName eq ‘Beverages’

W powyższym przykładzie Categories to encja która ma być zwrócona a parametr filter jest użyty do znalezienia kategorii które mają pole CategoryName równe ‘Beverages’. Dane mogą być zwracane w formacie XML (OData ATOM Format) lub JSON
Powyższe zapytanie zwraca wynik w postaci XML:

<?xml version="1.0" encoding="utf-8" ?>
<feedxml:base="http://localhost:5000/WcfDataService1.svc/"
xmlns=http://www.w3.org/2005/Atom
xmlns:d=http://schemas.microsoft.com/ado/2007/08/dataservices
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">

<id>http://localhost:5000/WcfDataService1.svc/Categories</id>
<titletype="text">Categories</title>
<updated>2013-01-01T23:54:24Z</updated>
<linkrel="self"title="Categories"href="Categories" />
<entry>
<id>http://localhost:5000/WcfDataService1.svc/Categories(1)</id>
<categoryterm="NorthwindsModel.Category"
scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />

<linkrel="edit"title="Category"href="Categories(1)" />
<title />
<updated>2013-01-10T23:54:24Z</updated>
<author>
<name />
</author>
<contenttype="application/xml">
<m:properties>
<d:CategoryIDm:type="Edm.Int32">1</d:CategoryID>
<d:CategoryName>Beverages</d:CategoryName>
<d:Description>Soft drinks, coffees, teas, beers, and ales</d:Description>
<d:Picturem:type="Edm.Binary">FRwvA…</d:Picture>
</m:properties>
</content>
</entry>
</feed>

Tworzenie WCF Data Service

Utworzenie serwisu WCF Data Service polega na utworzeniu aplikacji WEB, utworzeniu modelu Entity Framework i wyeksponowanie tego modelu dodając WCF Data Service:

  1. Dodaj pusty projekt ASP.NET Web Application w Visual Studio i nazwij go NorthwindsWCFDataService.
  2. Dodaj do projektu model ADO.NET Entity Data Model dla bazy Northwind z nazwą NorthwindsModel.
  3. Dodaj do projektu serwis WCF Data Service i nazwij go NorthwindsService.svc. Spowoduje to wygenerowanie następującej klasy dziedziczącej z DataService:
public class WCF_Data_Service : DataService< /* TODO: put your data source class name here */ >
{
// Thismethodis called only once to initialize service-wide policies.
public staticvoidInitializeService(DataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
// Examples:
// config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
// config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
}
}
  1. Zamień tekst TODO na nazwę modelu Entity Data Model czyli NorthwindsEntities.
  2. Dodaj poniższą linię kodu do metody InitializeService:
config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead );
  1. Uruchom projekt i otwórz adres w przeglądarce: http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/Categories

Serwisy WCF Data Service pozwalają na używanie następujących opcji zapytań:

  • $orderby - ustawia sortowanie np 
    $orderby=CategoryName,CategoryId
  • $top - ustawia limit pobierania danych np 
    $top=10
  • $skip - ustawia liczbę rekordów wynikowych do pominięcia np 
    $skip=10
  • $filter - definiuje wyrażenie filtrujące dane np 
    ?$filter=CategoryName eq ‘Beverages’
  • $expand - określa które powiązane encje mają być zawarte w wyniku np 
    $expand=Products
  • $select - określa kolumny zwracane w wyniku np: 
    $select=CategoryName,CategoryId
  • $inlinecount - zwraca liczbę wierszy wyniku

Lista operatorów używanych w opcji $filter:

  • Eq - równe
  • Ne - nierówne
  • Gt - większe niż
  • Ge - większe lub równe
  • Lt - mniejsze niż
  • Le - mniejsze lub równe
  • And - logiczne And
  • Or - logiczne Or
  • Not - logiczne Not
  • () - nawias matematyczny określający pierwszeństwo
  • Add - dodawanie
  • Sub - odejmowanie
  • Mul - mnożenie
  • Div - dzielenie
  • Mod - reszta

W opcji $filter można również używać następujących funkcji dla ciągów znaków:

  • bool substring(string p0, string p1) - zwraca true jeżeli p0 jest w p1.
  • bool endswith(string p0, string p1) - zwraca true jeżeli p0 zakończone jest ciągiem p1.
  • bool startswith(string p0, string p1) - zwraca true jeżeli p0 zaczyna sie od p1.
  • int length(string p0) - zwraca długość ciągu.
  • int indexof(string p0, string p1) - zwraca pozycję p0 w p1.
  • string replace(string p0, string p1, string replace) - wyszukuje p1 w p0 i zastępuje przez replace.
  • string substring(string p0, int pos) - zwraca podciąg p0 od pozycji pos.
  • string substring(string p0, int pospos, int length) - zwraca podciąg p0 od pozycji pos o długości length.
  • string tolower(string p0) - zwraca ciąg jako małe znaki.
  • string toupper(string p0) - zwraca ciąg jako duże znaki.
  • string trim(string p0) - zwraca ciąg bez białych znaków.
  • string concat(string p0, string p1) - łączy ciągi.

W opcji $filter można również używać następujących funkcji dla dat:

  • int day(DateTime p0) - zwraca dzień z daty.
  • int hour(DateTime p0) - zwraca godzinę z daty.
  • int minute(DateTime p0) - zwraca minuty z daty.
  • int month(DateTime p0) - zwraca miesiącz daty.
  • int second(DateTime p0) - zwraca sekundy z daty.
  • int year(DateTime p0) - zwraca rokz daty.

W opcji $filter można również używać następujących funkcji matematycznych:

  • double round(double p0) - zaokraglenie.
  • decimal round(double p0) - zaokrąglenie.
  • double floor(double p0) - podłoga.
  • decimal floor(double p0) - podłoga.
  • double ceiling(double p0) - powała.
  • decimal ceiling(double p0) - powała.

W opcji $filter można również używać następujących funkcji typów:

  • bool isOf(type p0) - zwraca true jeżeli encja jest typu p0.
  • bool IsOf(expression p0, type p0) - zwraca true jeżeli p0 jest typu p1.

Poza używaniem $filter można również pobierać encje używając klucza głównego w nawiasie okrągłym () np

http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/Categories(1)

Poniższy adres URI zwraca CategoryName tabeli Category z wartością klucza głównego równą 1:

http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/Categories(1)/CategoryName

Tworzenie aplikacji klienckiej używającej Uses WCF Data Services

W tej sekcji utworzymy aplikację konsolową która będzie odnosić się do Northwinds WCF Data Service i wykonywać operacje CRUD na danych:

  1. Dodaj nowy projekt Console Application w Visual Studio o nazwie NorthwindsClient.
  2. W oknie Solution Explorer kliknij prawym przyciskiem myszy na References i dodaj referencje do serwisu wybierając opcję Add Service Reference.
  3. Używając guzika Discover wybierz serwisy zawarte w solucji. Zmień przestrzeń nazw na NorthwindsServiceReference.
  4. Do klasy Program dodaj następujące klauzule using:
using NorthwindsClient.NorthwindsServiceReference;
usingSystem.Diagnostics;
usingSystem.Data.Services.Client;
usingSystem.Net;
  1. Dodaj do metody Main następujący kod:
NorthwndEntities db =
new NorthwndEntities(new Uri("http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/"));

var categories = from c in db.Categories select c;
foreach (Categories category in categories)
{
Debug.WriteLine(
string.Format("CategoryId: {0}, CategoryName: {1}", category.CategoryID, category.CategoryName));
}
  1. Ustaw aplikacje konsolową jako startową i uruchom projekt.

Kod odpytujący baz danych jest taki sam jak dla modelu ADO.NET Entity Framework. Jedyną różnicą jest utworzenie obiektu NorthwindsEntities który tworzymy podając adres URI do serwisu WCF Data Service.

Dodawanie rekordów za pomocą WCF Data Services

Poniższy kod dodaje nowy rekord do tabeli Categories:

NorthwndEntities db =
new NorthwndEntities(new Uri("http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/"));

// Create a category
Categories category = new Categories() {
CategoryName = "Alcohol",
Description = "Happy Beverages"
};

db.AddToCategories(category);
DataServiceResponse response = db.SaveChanges();
if (response.First().StatusCode == (int)HttpStatusCode.Created)
{
Debug.WriteLine("New CategoryId: {0}", category.CategoryID);
}
else
{
Debug.WriteLine("Error: {0}", response.First().Error.Message);
}

Istnieje tutaj kilka różnic w porównaniu do dodawania rekordów za pomocą ADO.NET Entity Framework
Po pierwsze w klasie NorthwindsEntities została wygenerowana metoda AddToCategories
Po drugie metoda SaveChanges zwraca obiekt DataServiceResponse który zawiera listę odpowiedzi z serwera. W powyższym przykładzie używamy właściwości StatusCode aby sprawdzić czy rekord został dodany. 
Jeżeli uruchomimy ten kod to zwrócony zostanie nam błąd “An Error Occurred While Processing This Request.”. Dzieje się tak ponieważ nie zezwoliliśmy na tworzenie rekordów w czasie ustawiania zabezpieczeń serwisu. Aby to zmienić należy dodać następujący wpis do metodyInitializeService serwisu:

config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead | 
EntitySetRights.AllWrite);

lub

config.SetEntitySetAccessRule("Categories", EntitySetRights.All);

Aktualizacja rekordów za pomocą WCF Data Services

Poniższy kod aktualizuje rekord z tabeli Categories:

NorthwndEntities db =
new NorthwndEntities(newUri("http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/"));

Categories category = db.Categories
.Where(c => c.CategoryName == "Alcohol")
.FirstOrDefault();

category.Description = "Happy People";

db.UpdateObject(category);

db.SaveChanges();

Metoda UpdateObject powoduje, że obiekt będzie zaktualizowany po wywołaniu metody SaveChanges.

Usuwanie rekordów za pomocą WCF Data Services

Poniższy kod usuwa rekord z tabeli Categories:

NorthwndEntities db =
new NorthwndEntities(newUri("http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/"));

Categories category = db.Categories
.Where(c => c.CategoryName == "Alcohol")
.FirstOrDefault();

db.DeleteObject(category);

db.SaveChanges();

Metoda DeleteObject usuwa rekord po wywołaniu metody SaveChanges.

Pobranie danych w formacie JSON

Domyślnie dane są zwracane w formacie XML. Aby otrzymać dane w formacie JSON należy dołączyć nagłówek do żądania HTTP o następującej treści:

“Accept: application/json;odata=verbose

Poniższy kod tworzy żądanie używając obiektu WebRequest pobierające dane w postaci JSON:

HttpWebRequest req =
(HttpWebRequest)
WebRequest.Create(
"http://localhost/NorthwindsWCFDataService/NorthwindsService.svc/Categories(1)?$select=CategoryID,CategoryName,Description");

req.Accept = "application/json;odata=verbose";

using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
{
Stream s = resp.GetResponseStream();
StreamReader readStream = new StreamReader(s);

string jsonString = readStream.ReadToEnd();

Debug.WriteLine(jsonString);

resp.Close();
readStream.Close();
}

Wynik działania kodu:

{
"d":
{
"__metadata":
{
"id":"http://localhost:8999/NorthwindsService.svc/Categories(1)",
"uri":"http://localhost:8999/NorthwindsService.svc/Categories(1)",
"type":"NorthwindsModel.Category"
}
,
"CategoryID":1,
"CategoryName":"Beverages",
"Description":"Soft drinks, coffees, teas, beers, and ales"
}
}

Operacje I/O

Operacje I/O są operacjami odczytu i zapisu plików. Pliki są przechowywane w katalogach a .NET Framework zapewnia zestaw klas służących do kopiowania, przenoszenia, usuwania i sprawdzania czy plik lub katalog istnieje. Plik jest uporządkowaną i nazwaną kolekcją bajtów które zostały zapisane do magazynu danych. Kiedy pracujemy z plikami używamy strumieni. Strumień jest obiektem w pamięci używanym do reprezentowania sekwencji bajtów w pliku. Specjalne klasy odczytujące i zapisujące służą do pracy z zakodowanymi strumieniami. Wszystkie typy służące do pracy z operacjami I/O znajdują się w przestrzeni nazw System.IO.

Pliki i foldery

Lista klas służących do pracy z plikami i folderami:

  • File - klasa statyczna posiadająca metody do tworzenia, kopiowania, usuwania, przenoszenia i otwierania plików.
  • FileInfo - klasa instancyjna posiadająca metody do tworzenia, kopiowania, usuwania, przenoszenia i otwierania plików.
  • Directory - klasa statyczna posiadająca metody do tworzenia, kopiowania, usuwania, przenoszenia i enumerowania przez pliki w katalogu.
  • DirectoryInfo - klasa instancyjna posiadająca metody do tworzenia, kopiowania, usuwania, przenoszenia i enumerowania przez pliki w katalogu.
  • Path - klasa statyczna która dostarcza sposobów uzyskiwania informacji lub manipulowania plikami lub katalogami używając ciągu znaków.

Klasy File i FileInfo są podobne z wyjątkiem tego, że File jest klasą statyczną i nie posiada właściwości. 
Właściwości klasy FileInfo:

  • Directory - pobiera instancję obiektu DirectoryInfo dla katalogu nadrzędnego.
  • DirectoryName - pobiera ciąg znaków dla pełnej ścieżki katalogu.
  • Exists - zwraca true jeżeli plik istnieje.
  • IsReadOnly - zwraca true jeżeli plik jest tylko do odczytu.
  • Length - zwraca rozmiar pliku w bajtach.
  • Name - zwraca nazwę pliku.

Klasa FileInfo dziedziczy z klasy System.IO.FileSystemInfo która zawiera właściwości dla atrybutów, czasu utworzenia, pełnej nazwy, ostatniego czasu dostępu i ostatniego czasu zapisu do pliku. 
Konstruktor klasy FileInfo pobiera ciąg znaków ze ścieżką i nazwą pliku, np:

FileInfo fileInfo = new FileInfo(@”c:\Samples\HelloWorld.txt”);
Debug.WriteLine(fileInfo.Name);

Lista metod klas File i FileInfo jest taka sama ale ich parametry różnią się od siebie. Metody klasy File wymagają podania ścieżki pliku. 
Lista najczęściej wykorzystywanych metod klas File i FileInfo:

  • AppendAllText - tworzy obiekt klasy StreamWriter który może być użyty do dodania tekstu do pliku.
  • CopyTo (FileInfo) - kopiuje plik.
  • Copy (File) - kopiuje plik.
  • Create - tworzy plik.
  • Decrypt - odszyfrowuje plik który został zaszyfrowany przez aktualne konto.
  • Delete - usuwa plik.
  • Encrypt - szyfruje plik.
  • MoveTo - przenosi plik.
  • Open - zwraca obiekt klasy FileStream umożliwiający odczyt i zapis.
  • Replace - zastępuje treść pliku treścią innego pliku.
  • SetAccessControl - stosuje listę kontroli dostępu za pomocą obiektu FileSecurity.

Klasy Directory i DirectoryInfo są podobne do klas File i FileInfo z tym, że operują na folderach a nie na plikach. 
Właściwości klasy DirectoryInfo:

  • Exists - zwraca true jeżeli plik istnieje.
  • Name - zwraca nazwę pliku.
  • Parent - zwraca obiekt klasy DirectoryInfo dla folderu nadrzędnego.
  • Root - zwraca obiekt klasy DirectoryInfo dla folderu głównego.

Lista najczęściej wykorzystywanych metod klas Directory i DirectoryInfo:

  • Create (DirectoryInfo) - tworzy folder.
  • CreateDirectory (Directory) - tworzy folder.
  • Delete - usuwa folder.
  • GetAccessControl - zwraca obiekt DirectorySecurity zawierający listę kontroli dostępu.
  • GetDirectories - zwraca tablicę obiektów DirectoryInfo dla podkatalogów.
  • GetFiles - zwraca tablicę obiektów FileInfo dla plików w katalogu.
  • GetFileSystemInfos - zwraca tablicę obiektów FileSystemInfo dla plików i katalogów w danym katalogu.
  • MoveTo (DirectoryInfo) - przenosi folder.
  • MoveTo (Directory) - przenosi folder.
  • SetAccessControl - stosuje listę kontroli dostępu za pomocą obiektu DirectorySecurity.

Poniższy przykład wyświetla listę plików i katalogów na dysku C:

//DirectoryInfo
DirectoryInfo directoryInfo = new DirectoryInfo(@"c:\");

//Directories
Debug.WriteLine("Directories");
foreach (FileInfo fileInfo in directoryInfo.GetFiles())
{
Debug.WriteLine(fileInfo.Name);
}

//Files
Debug.WriteLine("Files");
foreach (DirectoryInfo diin directoryInfo.GetDirectories())
{
Debug.WriteLine(di.Name);
}

Strumienie

Strumienie są klasami wykorzystywane do przechowywania zawartości pliku. 
Lista strumieni dostępnych w .NET Framework:

  • FileStream - odczytują i zapisują pliki.
  • IsolatedStorageFileStream - odczytują i zapisują pliki w odizolowanych magazynach.
  • MemoryStream - odczytują i zapisują w pamięci.
  • BufferedStream - używane do przechowywania bloków bajtów w pamięci.
  • NetworkStream - zapisują i odczytują dane w sieci używając gniazd (ang. socket).
  • PipeStream - zapisują i odczytują dane w sieci za pomocą kanałów zwanych pipe.
  • CryptoStream - używane do łączenia danych z transformacjami kryptograficznymi.

Klasa FileStream może by użyta do odczytu, zapisu, otwarcia i zamknięcia plików. Poniższy przykład tworzy nowy plik, zapisuje w nim numery i zamyka plik:

FileStream fileStream = new FileStream(
@"d:\Samples\Numbers.txt",
FileMode.Truncate,
FileAccess.Write,
FileShare.None)
;
for (int i = 0; i < 10; i++)
{
byte[] number = new UTF8Encoding(true).GetBytes(i.ToString());
fileStream.Write(number, 0, number.Length);
}

fileStream.Close();

Konstruktor FileStream pobiera cztery parametry, ścieżkę do pliku, tryb pliku, dostęp do pliku i udostępnienie pliku. 
Enumerator FileMode określa czy tworzymy, otwieramy czy czyścimy plik. 
Wartości enumeratora FileMode:

  • Append - otwiera plik jeżeli istnieje i szuka na koncu pliku lub tworzy nowy plik jeżeli nie istnieje. Może być używany tylko z dostępem FileAccess.Write.
  • CreateNew - tworzy nowy plik, jeżeli plik istnieje to zgłasza wyjątek.
  • Create - tworzy nowy plik, jeżeli plik istnieje to go nadpisuje. Jeżeli plik istnieje ale jest ukryty to zgłoszony zostaje wyjątek.
  • Open - otwiera plik lub zgłasza wyjątek jeżeli plik nie istnieje.
  • OpenOrCreate - otwiera plik lub tworzy nowy jeżeli nie istnieje.
  • Truncate - otwiera plik i czyści jego dane. Jeżeli plik nie istnieje to zgłasza wyjątek.

Enumerator FileAccess określa co robimy ze strumieniem po jego utworzeniu. 
Wartości enumeratora FileAccess:

  • Read - odczyt pliku.
  • Write - zapis do pliku.
  • ReadWrite - odczyt i zapis do pliku.

Enumerator FileShare określa typ dostępu do pliku dla innych strumieni w czasie kiedy jest on otwarty. 
Wartości enumeratora FileShare:

  • None - blokuje dostęp do pliku.
  • Read - pozwala na odczyt.
  • Write - pozwala na zapis.
  • ReadWrite - pozwala na odczyt i zapis.
  • Delete - pozwala na usuwanie.
  • Inheritable - sprawia, że uchwyt pliku jest dziedziczony przez procesy.

Kiedy tworzymy lub otwieramy plik to proces musi mieć odpowiednie uprawnienia do pliku lub katalogu aby mógł wykonać określone operacje. typy uprawnień są opisane w enumeracji System.Security.Permissions.FileIOPermissionAccess:

  • NoAccess - brak dostępu.
  • Read - pozwala na odczyt.
  • Write - pozwala na zapis.
  • Append - pozwala na dodanie danych a w tym na tworzenie nowych plików i folderów.
  • PathDiscovery - dostęp do informacji o ścieżce.
  • AllAccess - wszystkie powyższe uprawnienia.

Klasy odczytujące i zapisujące

Klasy odczytujące i zapisujące (ang. readers i writers) są klasami przestrzeni nazw System.IO które odczytują lub zapisują zakodowane znaki ze strumieni. 
Typy klas odczytujących i zapisujących w przestrzeni nazw System.IO:

  • BinaryReader, BinaryWriter - używany do odczytu i zapisu danych binarnych.
  • StreamReader, StreamWriter - używany do odczytu i zapisu znaków używając zakodowanej wartości do konwertowania znaków do i z bajtów.
  • StringReader, StringWriter - używany do odczytu i zapisu znaków do i z ciągów znaków.
  • TextReader, TextWriter - klasy abstrakcyjne dla innych klas odczytujących lub zapisujących.

Klasy StreamReader i StringReader dziedziczą z klasy abstrakcyjnej TextReader
Klasy StreamWriter i StringWriter dziedziczą z klasy abstrakcyjnej TextWriter.

Klasa StreamReader jest używana do odczytu znaków w danym kodowaniu. Domyślnym kodowaniem jest UTF-8. Klasy StreamReader można użyć do odczytania standardowego pliku tekstowego. 
Najczęściej wykorzystywane metody klasy StreamReader:

  • Close - zamyka czytnik strumienia i strumień.
  • Peek - zwraca następny znak ze strumienia ale nie przechodzi do jego pozycji.
  • Read() - zwraca następny znak ze strumienia i przechodzi do jego pozycji.
  • Read(Char[], Int32, Int32) - odczytuje określoną liczbę znaków do tablicy bajtowej.
  • ReadBlock(Char[], Int32, Int32) - odczytuje określoną liczbę znaków do tablicy bajtowej.
  • ReadLine - odczytuje linię tekstu i zwraca ciąg znaków.
  • ReadToEnd - odczytuje wszystkie znaki od aktualnej pozycji do końca pliku i zwraca ciąg znaków.

Poniższy kod wyświetla treść pliku znak po znaku, linię po linii oraz całą zawartość naraz za pomocą klasy StreamReader:

StreamReader streamReader = new StreamReader(@"d:\Samples\Numbers.txt");
Debug.WriteLine("Char by Char");
while (!streamReader.EndOfStream)
{
Debug.WriteLine((char)streamReader.Read());
}
streamReader.Close();

streamReader = new StreamReader(@"d:\Samples\Numbers.txt");
Debug.WriteLine("Line by line");
while (!streamReader.EndOfStream)
{
Debug.WriteLine(streamReader.ReadLine());
}
streamReader.Close();

streamReader = new StreamReader(@"d:\Samples\Numbers.txt");
Debug.WriteLine("Entire file");
Debug.WriteLine(streamReader.ReadToEnd());

Klasa StringReader jest podobna do klasy StreamReader z wyjątkiem tego, że zamiast odczytywać plik odczytujemy ciąg znaków. Konstruktor klasy StringReader pobiera ciąg znaków jako parametr. 
Poniższy kod wyświetla treść pliku znak po znaku za pomocą klasy StringReader:

StringReader stringReader = new StringReader("Hello\nGoodbye");
int pos = stringReader.Read();

while (pos != -1)
{
Debug.WriteLine("{0}", (char)pos);
pos = stringReader.Read();
}

stringReader.Close();

Klasa StreamWriter jest podobna do klasy Stream z wyjątkiem tego, że klasa StreamWriter odczytuje znaki w określonym kodowaniu a klasa Stream jest zaprojektowana do bajtowego wejścia i wyjścia. Klasy StreamWriter można użyć do zapisu danych do pliku. Klasa StreamWriterposiada metody Write i WriteLine służące do zapisu strumienia do pamięci. Klasa StreamWriter posiada właściwość AutoFlush która kiedy jest ustawiona na true to powoduje, że zapis do zasobu jest wykonywany po wywołaniu metody Write a kiedy jest ustawiona na false to zapis do zasobu jest wykonywany po wywołaniu metody Flush lub kiedy StreamWriter jest zamykany. 
Poniższy kod zapisuje wartości do pliku:

StreamWriter streamWriter = new
StreamWriter(@"d:\Samples\StreamWriter.txt");

streamWriter.WriteLine("ABC");
streamWriter.Write(true);
streamWriter.Write(1);

streamWriter.Close();

Parametr konstruktora przyjmuje ścieżkę do pliku. jeżeli plik istnieje to jest nadpisywany lub jest tworzony jeżeli nie istnieje. Do konstruktora można również przekazać strumień.

Klasy BinaryWriter używamy do zapisu typów prostych binarnie lub jako ciągów znaków w wybranym kodowaniu. 
Poniższy kod zapisuje dane w formacie binarnym:

FileStream fileStream = new FileStream(@"d:\Samples\BinaryWriter.txt",
FileMode.Create);
BinaryWriter binaryWriter = new BinaryWriter(fileStream);

binaryWriter.Write("ABC");
binaryWriter.Write(true);
binaryWriter.Write(1);

binaryWriter.Close();

Klasa BinaryWriter potrzebuje obiektu Stream przekazanego do konstruktora.

Aby odczytać dane binarne należy użyć klasy BinaryReader
Poniższy kod odczytuje plik utworzony poprzednio:

FileStream fileStream = new FileStream(@"d:\Samples\BinaryWriter.txt",
FileMode.Open);
BinaryReader binaryReader = new BinaryReader(fileStream);

string abs = binaryReader.ReadString();
bool b = binaryReader.ReadBoolean();
int i = binaryReader.ReadInt32();

binaryReader.Close();

Asynchroniczne Operacje I/O

Klasy StreamReader i Writer zapewniają możliwość odczytu i zapisu plików asynchronicznie. 
Wykonanie asynchroniczne odbywa się z użyciem słów kluczowych async i await.

Poniższy fragment kodu demonstruje asynchroniczne przeszukiwanie plików w folderze które zawierają określony ciąg znaków. Jeżeli plik zawiera szukaną frazę to jego nazwa jest wyświetlana:

privateasyncvoidbutton1_Click(object sender, EventArgs e)
{
this.Text = "Searching...";

string outputFileName = @"d:\Samples\FoundFiles.txt";

await SearchDirectory(@"d:\Samples", "A", outputFileName);

this.Text = "Finished";

Process.Start(outputFileName);
}

privatestaticasync Task SearchDirectory(string searchPath, string searchString,
string outputFileName
)
{
StreamWriter streamWriter = File.CreateText(outputFileName);

string[] fileNames = Directory.GetFiles(searchPath);
await FindTextInFilesAsync(fileNames, searchString, streamWriter);

streamWriter.Close();
}

privatestaticasync Task FindTextInFilesAsync(string[] fileNames, string
searchString, StreamWriter outputFile
)
{
foreach (string fileName in fileNames)
{
if (fileName.ToLower().EndsWith(".txt"))
{
StreamReader streamReader = new StreamReader(fileName);

string textOfFile = await streamReader.ReadToEndAsync();
streamReader.Close();

if (textOfFile.Contains(searchString))
{
await outputFile.WriteLineAsync(fileName);
}
}
}
}

Serializacja

Serializacja jest procesem transformacji obiektu do innej formy która może być przechowywana w magazynie danych lub przesyłana z jednej domeny aplikacji do innej. Proces odwrotny do serializacji to deserializacja. Obiekty można serializować do zapisu na dysku, do strumienia, do pamięci lub do przesłania przez sieć. Najbardziej popularnymi formatami serializacji są XML i JSON.NET Framework posiada klasy które wspierają serializację binarną, XML i JSON. Można również utworzyć własny sposób serializacji.

Serializacja binarna

Klasa BinaryFormatter jest używana do binarnej serializacji obiektów. Można ją znaleźć w przestrzeni nazw System.Runtime.Serialization.Formatters.Binary. Dwie najważniejsze metody tej klasy to Serialize i Deserialize
Klasy BinaryFormatter można użyć ze strumieniem FileStream do odczytu i zapisu obiektów na dysk. Klasa która ma być serializowana musi posiadać atrybut [Serializable]
Poniższy fragment kodu tworzy klasę Person i czyni ją serializowalną:

[Serializable]
classPerson
{
privateint _id;
publicstring FirstName;
publicstring LastName;

publicvoidSetId(int id)
{
_id = id;
}
}

Jeżeli chcemy zapisać obiekt do magazynu danych to możemy utworzyć instancję klasy BinaryFormatter i wywołać metodę Serialize:

Person person = new Person();
person.SetId(1);
person.FirstName = "Joe";
person.LastName = "Smith";

IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("Person.bin", FileMode.Create, FileAccess.Write,
FileShare.None);
formatter.Serialize(stream, person);
stream.Close();

Należy zauważyć, że nawet prywatne zmienne są zapisywane na dysk.

Aby odtworzyć obiekt z pliku Person.bin należy użyć metody Deserialize:

stream = new FileStream("Person.bin",FileMode.Open,FileAccess.Read,FileShare.Read);
Person person2 = (Person)formatter.Deserialize(stream);
stream.Close();

Aby zapobiec serializacji pola klasy należy użyć atrybutu [NonSerialized], np:

[Serializable]
classPerson
{
[NonSerialized]
privateint _id;
publicstring FirstName;
publicstring LastName;
publicvoidSetId(int id)
{
_id = id;
}
}

Serializacja XML

Do serializacji XML używamy klasy XmlSerializer z przestrzeni nazw System.Xml.Serialization. Klasa XmlSerializer serializuje jedynie klasy publiczne i ich publiczne właściwości i pola oraz nie wymaga użycia atrybutu [Serializable]
Poniższy fragment kodu serializuje obiekt klasy Person:

Personperson = new Person();
person.SetId(1);
person.FirstName = "Joe";
person.LastName = "Smith";

XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));
StreamWriter streamWriter = new StreamWriter("Person.xml");
xmlSerializer.Serialize(streamWriter, person);

Wynik działania kodu:

<?xml version="1.0" encoding="utf-8"?>
<Personxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">

<FirstName>Joe</FirstName>
<LastName>Smith</LastName>
</Person>

Aby pole było ignorowane podczas serializacji należy użyć atrybutu [XmlIgnore], np:

[XmlIgnore]
publicstring FirstName;

Aby odtworzyć obiekt z pliku XML należy użyć metody Deserialize:

XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));
FileStream fs = new FileStream("Person.xml", FileMode.Open);
Personperson = (Person)xmlSerializer.Deserialize(fs);
Atrybuty używane przez XmlSerializer:
  • XmlIgnore - ignoruje pole w czasie serializacji.
  • XmlAttribute - mapuje pole jako atrybut.
  • XmlElement - mapuje pole jako węzeł.
  • XmlArray - używany przy serializacji kolekcji.
  • XmlArrayItem - używany przy serializacji kolekcji.
Przykład atrybutów serializacji kolekcji:
[Serializable]
publicclassOrder
{
 [XmlAttribute]
 publicint ID { get; set; }

 [XmlIgnore]
 publicbool IsDirty { get; set; }

 [XmlArray(“Lines”)]
 [XmlArrayItem(“OrderLine”)]
 public List<OrderLine> OrderLines { get; set; }
}

Serializacja JSON

JSON jest podobny do XML z tym, że jest mniej obszerny. Aby serializować do postaci JSON trzeba jawnie wstawić atrybut przed każdą właściwością która ma być serializowana. Ponadto trzeba użyć atrybutu klasy [DataContract]
Poniższy fragment kodu pokazuje jak należy zmodyfikować klasę Person aby możliwa była serializacja JSON:

[DataContract]
publicclassPerson
{
[DataMember]
privateint _id;
[DataMember]
publicstring FirstName;
[DataMember]
publicstring LastName;

publicvoidSetId(int id)
{
_id = id;
}
}

Aby zignorować pole należy nie umieszczać atrybutu [DataMember] przed jego deklaracją. 
Poniższy fragment kodu demonstruje serializację obiektu klasy Person do JSON
Person person = new Person();

person.SetId(1);
person.FirstName = "Joe";
person.LastName = "Smith";

Stream stream = new FileStream("Person.json", FileMode.Create);
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Person));
ser.WriteObject(stream, person);
stream.Close();

Zamiast metody Serialize używamy metody WriteObject
Wynik działania kodu:

{
"FirstName":"Joe",
"LastName":"Smith",
"_id":1
}

Aby odczytać obiekt z postaci JSON należy użyć metody ReadObject:

Personperson = new Person();

Stream stream = new FileStream("Person.json", FileMode.Open);
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Person));
person = (Person)ser.ReadObject(stream);
stream.Close();

Serializacja niestandardowa

Istnieją dwa sposoby dostosowywania procesu serializacji. Pierwszym jest dodanie metody z odpowiednim atrybutem która będzie manipulowała danymi obiektu w czasie serializacji i deserializacji obiektu. Atrybuty których można użyć to OnDeserializedAttribute,OnDeserializingAttributeOnSerializedAttribute i OnSerializingAttribute. Dodanie tych atrybutów przed metodami powoduje ich uruchomienie w wybranej fazie procesu serializacji lub deserializacji. 
Poniższy fragment kodu dostosowuje logikę serializacji klasy Person:

[OnSerializing()]
internal void OnSerializingMethod(StreamingContext context)
{
FirstName = "Bob";
}

[OnSerialized()]
internal void OnSerializedMethod(StreamingContext context)
{
FirstName = "Serialize Complete";
}

[OnDeserializing()]
internal void OnDeserializingMethod(StreamingContext context)
{
FirstName = "John";
}

[OnDeserialized()]
internal void OnDeserializedMethod(StreamingContext context)
{
FirstName = "Deserialize Complete";
}

Drugim sposobem dostosowania serializacji jest implementacja interfejsu ISerializable. Interfejs ten posiada jedną metodę którą trzeba zaimplementować GetObjectData. Metoda ta jest wywoływana kiedy obiekt jest serializowany. Należy zaimplementować również specjalny konstruktor który będzie wywołany podczas deserializacji. 
Poniższy fragment kodu zmienia klasę Person tak aby implementowała interfejs ISerializable:

[Serializable]
publicclassPerson : ISerializable
{
privateint _id;
publicstring FirstName;
publicstring LastName;

publicvoidSetId(int id)
{
_id = id;
}

publicPerson() { }

publicPerson(SerializationInfo info, StreamingContext context)
{
FirstName = info.GetString("custom field 1");
LastName = info.GetString("custom field 2");
}

publicvoidGetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("custom field 1", FirstName);
info.AddValue("custom field 2", LastName);
}
}

Metoda GetObjectData pobiera parametr typu SerializationInfo która pozwala dostosować nazwę i dane, które zostaną zapisane do strumienia. 
Wynik serializacji do JSON będzie następujący:

{
"custom_x0020_field_x0020_1":"Joe",
"custom_x0020_field_x0020_2":"Smith"
}

Kiedy obiekt jest deserializowany to wywoływany jest konstruktor z parametrem typu SerializationInfo. Można go użyć do odtworzenia obiektu za pomocą użytych nazw niestandardowych.

W większości przypadków zalecane jest użycie atrybutów zamiast implementowania interfejsu ISerializable.

Podsumowanie

Tablice i kolekcje

  • Tablice dziedziczą z typu System.Array.
  • Kolekcje ArrayListHashTableQueueSortedList, i Stack znajdują się w przestrzeni nazw System.Collections.
  • Kolekcje Dictionary<TKey, TValue>ListQueueSortedList<TKey, TValue> i Stack znajdują się w przestrzeni nazw System.Collections.Generic.
  • Kolekcja Queue jest typu first-in-first-out.
  • Kolekcja Stack jest typu last-in-first-out.
  • Aby porównywać obiekty należy zaimplementować interfejs IComparable.
  • Kolekcja Dictionary przechowuje pary klucz/wartość.
  • Kolekcje niestandardowe dziedziczą z klasy bazowej CollectionBase.

ADO.NET

  • ADO.NET jest zestawem klas używanych do wykonywania komend na bazie danych.
  • Obiekt Command jest używany do wykonywania procedur składowanych lub instrukcji SQL.
  • Metoda ExecuteNonQuery jest używana do wykonywania instrukcji które nie zwracają wyników jak np INSERT lub UPDATE.
  • Metoda ExecuteScalar jest używana do pobierania tylko jednej wartości z bazy danych.
  • Obiekt DBDataReader jest kursorem tylko do odczytu połączonym z bazą danych który odczytuje dane tylko w przód.
  • Metoda ExecuteXMLReader zwraca dane w postaci XML. Aby pobrać dane w postaci XML z bazy SQL Server należy użyć klauzuli FOR XML.
  • Obiekt DataSet jest odłączonym od bazy zbiorem wyników i może posiadać jedną lub więcej tabel wynikowych DataTables. Klasa DataAdapter jest używana do wypełnienia DataSet.
  • Klasa DataAdapter może być użyta z DataSet do dodawania, aktualizacji lub usuwania rekordów z bazy danych.

ADO.NET Entity Framework

  • Entity Framework jest narzędziem typu ORM które używa ADO.NET do komunikacji z baza danych.
  • Entity Framework Model zawiera klasy reprezentujące obiekty w bazie danych.
  • Entity Framework Model procedury składowane są zmapowane jako metody.

WCF Data Services

  • WCF Data Services pozwalają na dostęp do bazy danych przez sieć.
  • WCF Data Services używają protokołu OData.
  • WCF Data Services domyslnie zwracają dane w formacie OData ATOM ale mogą również zwracać je jako JSON.
  • Dane w bazie danych można odpytywać za pomocą ciągów zapytań w adresie URL.

I\O

  • Klasy File i FileInfo sa używane do uzyskiwania właściwości plików oraz wykonywania operacji na plikach.
  • Obiekt strumienia Stream jest używany jako reprezentacja pliku w pamięci i może być używany do zapisu i odczytu danych w pliku.
  • Klasy BinaryReader i BinaryWriter są używane do zapisu i odczytu plików w postaci binarnej.
  • Klasy StreamReader i StreamWriter są używane do zapisu i odczytu znaków poprzez użycie kodowanej wartości do konwertowania znaków do i z postaci binarnej. Domyślnym kodowaniem jest UTF-8.
  • Za pomocą obiektu StreamReader można odczytywać plik znak po znaku, linia po linii lub cały na raz.
  • Klasy StringReader i StringWriter odczytują i zapisują ciągi znaków.

Serializacja

  • Serializacja jest procesem transformacji obiektu do innej formy która może być przechowywana w magazynie danych lub przesyłana z jednej domeny aplikacji do innej.
  • Klasa BinaryFormatter służy do serializacji binarnej.
  • Klasa XmlSerializer służy do serializacji XML.
  • Klasa DataContractJsonSerializer służy do serializacji JSON.
  • Serializację można modyfikować używając atrybutów lub implementując interfejs ISerializable.