Category Archives: C#

.NET Core oraz ASP.NET Core

.NET Core to nowy framework od Microsoft’u, który aktualnie wciąż jest w produkcji. Zdecydowałem się napisać dzisiejszy post, ponieważ w ostatnim czasie nastąpiło wiele zmian w wersjonowaniu i nazewnictwie .NET. Moim zdaniem wprowadziło to trochę zamieszania.

Przede wszystkim, co to jest .NET Core? To nowa wersja framework’a, która z założenia ma być wieloplatformowa oraz open-source. To wielka zmiana ze strony Microsoft, ale już od kilku lat można obserwować, że Microsoft zmierzą w kierunku, który w latach 90 był kompletnie nie do pomyślenia. .NET Core będzie zatem działać zarówno na Windows jak i Mac czy Linux.

Druga różnica to fakt, że .NET Core to po prostu pakiet NuGet. Doskonale nadaje się to do aplikacji webowych. Dzisiaj, serwer web musi mieć zainstalowany .NET Framework. Oznacza to, że ciężko mieć kilka aplikacji ASP.NET uruchomianych w różnych wersjach .NET Framework.  Dzisiejsza wersja .NET Framework jest bardzo mocno powiązana z systemem operacyjnym i z punktu architektonicznego stanowi monolit.

Wchodząc trochę w szczegóły techniczne, .NET Core składa się z podstawowej biblioteki klas (CoreFx), oraz środowiska uruchomieniowego CoreClr. Na GitHub można śledzić postęp prac:

https://github.com/dotnet/corefx

https://github.com/dotnet/coreCLR

Ktoś może zapytać się, ale co z Mono? Aktualnie jest to implementacja .NET na Linuxa. Po wprowadzeniu .NET Core nie jest jeszcze jasne co stanie się z Mono, ponieważ jest to niezależny framework. Na dzień dzisiejszy, wydaje mi się, że w pewnym czasie .NET Core zastąpi całkowicie Mono.

Wprowadzenie .NET Core nie oznacza, że nagle będziemy mogli uruchamiać wszystkie aplikacje na Linux. .NET Core to jedynie podzbiór .NET Framework. Wiadomo, że główną motywacją były aplikacje webowe czyli ASP.NET. Inne typy aplikacji typu WPF nie będą oczywiście dostępne (przynajmniej bazując na informacjach, które są już dostępne).

Nie trudno teraz domyślić się, że ASP.NET Core to nowa wersja ASP.NET,  napisana pod .NET Core. Oznacza to, że będzie mogłaby być hostowana na Linux. Analogicznie do .NET Core, ASP.NET Core jest również open-source.

ASP.NET 5, o którym pisałem wiele razy na blogu, został zmieniony na ASP.NET Core. Informacje, które wcześniej podawałem,  dotyczą zatem ASP.NET Core, a nie ASP.NET 5, którego nazwa została zmieniona.  Jedną z większych zmian ASP.NET Core to wprowadzenie projects.json, zamiast packages.config. Więcej szczegółów można znaleźć w poprzednich wpisach.

Podsumowując, .NET Core oraz ASP.NET Core dadzą nam:

  • Wieloplatformowość
  • Modularność i niezależność od .NET Framework zainstalowanego na systemie – każda aplikacja, będzie mogą korzystać z innej wersji framework’a.
  • Open-source

Warto również przejrzyj się następującemu diagramowi (źródło  http://www.hanselman.com):

Jak widzimy, ASP.NET Core 1.0, będzie działać również na starym, monolitowym frameworku (.NET Framework 4.6). Taka konfiguracja też jest możliwa tzn. ASP.NET Core  + .NET Framework, ale w celu uzyskania wieloplatformowości należy zainteresować się ASP.NET Core 1.0 + .NET Core.

c# 7.0 – operator Is, wzorce rekurencyjne

Dzisiaj przyszedł czas na wzorzec agregujący w pewien sposób wszystkie poprzednie, a mianowicie wzorzec rekurencyjny. Załóżmy, że mamy następującą hierarchię klas, np.:

    public class Employee
    {
        public string Name { get; set; }
        public object Salary { get; set; }
    }

    public class Manager : Employee
    {
        public object DirectReports { get; set; }
    }

W poprzednich wersjach C#, było możliwe jedynie sprawdzenie czy obiekt jest typu “Manager” czy innej implementacji. W wersji 7.0 możemy:

            object employee = new Manager()
            {
                Salary = 53,
                DirectReports = new Employee[5]
            };

            if (employee is Manager { Salary is int salary,DirectReports is Employee[] directReports})
            {
                Console.WriteLine("It's a manager with salary {0} and direct reports {1}", salary, directReports.Length);
            }

Widzimy tutaj wzorzec rekurencyjny składający się z:

  • type pattern – sprawdzamy czy obiekt jest typu Manager
  • zagnieżdżonego type pattern – sprawdzamy czy Salary jest liczbą całkowitą
  • zagnieżdżonego type pattern – sprawdzamy czy DirectReports jest tablicą.

Jeśli któryś z warunków jest nieprawdziwy, całość zwróci false. Jeśli np. zamienimy tablicę na kolekcję, wtedy na ekranie nic się nie wyświetli:

        private static void Main(string[] args)
        {
            object employee = new Manager()
            {
                Salary = 53,
                DirectReports = new List<Employee>()
            };

            if (employee is Manager { Salary is int salary,DirectReports is Employee[] directReports})
            {
                Console.WriteLine("It's manager with salary {0} and direct reports {1}", salary, directReports.Length);
            }
        }

Można również umieścić “constant pattern”, o którym pisałem w poprzednim poście:

            object employee = new Manager()
            {
                Name="Piotr"
            };

            if (employee is Manager { Name is "Piotr"})
            {
                Console.WriteLine("Name: Piotr");
            }

W tym przypadku, instrukcja zwróci true, gdy właściwość Name ma wartość “Piotr”. Constant pattern, nie wyodrębnia zawartości, jedynie sprawdza czy zmienna jest równa danej stałej.

Zmieńmy teraz trochę strukturę klas na:

    public class Employee
    {
        public string Name { get; set; }
    }

    public class Manager : Employee
    {
        public object DirectReport { get; set; }
    }

Za pomocą wzorca możemy rekurencyjnie sprawdzać właściwości o dowolnym zagnieżdżeniu, tzn.:

            object employee = new Manager()
            {
                DirectReport = new Employee { Name = "Piotr" }
            };

            if (employee is Manager { DirectReport is Employee { Name is string name}})
            {
                Console.WriteLine(name);
            }

Rekurencyjna jest zatem przerywana, gdy jakiś warunek zwraca false. Jeśli zwraca true, wtedy kolejne warunki będą sprawdzane, przeglądając graf obiektów o dowolnej głębokości. W powyższym przykładzie, zatem musimy mieć obiekt typu Manager, w którym DirectReport jest typu Employee, a właściwość Name jest napisem.

Tak jak zwykle, na zakończenie zobaczmy kod po kompilacji:

		private static void Main(string[] args)
		{
			object employee = new Manager
			{
				Salary = 53,
				DirectReports = new List&lt;Employee&gt;()
			};
			Manager manager = employee as Manager;
			int salary;
			Employee[] directReports;
			bool arg_65_0;
			if (manager != null)
			{
				int? num = manager.Salary as int?;
				salary = num.GetValueOrDefault();
				if (num.HasValue)
				{
					directReports = (manager.DirectReports as Employee[]);
					arg_65_0 = (directReports != null);
					goto IL_65;
				}
			}
			arg_65_0 = false;
			IL_65:
			bool flag = arg_65_0;
			if (flag)
			{
				Console.WriteLine("It's manager with salary {0} and direct reports {1}", salary, directReports.Length);
			}
		}

Wyraźnie widzimy, że wszystkie warunki muszą zostać spełnione tzn.:

  • Employee musi być typu Manager
  • Salary jest liczbą całkowitą
  • DirectReports jest tablicą Employee

Ponadto, jak widzimy z powyższego kodu, typy proste mogą być nullable, o ile mają w sobie jakąś wartość. Zatem salary mogłaby być typu int?, o ile wartość nie jest NULL.

c# 7.0 – operator Is, część druga

Ostatnio przyjrzeliśmy się pierwszemu usprawnieniu operatora IS. Tak naprawdę jest to część dużo większej zmiany, a mianowicie dopasowywania wzorców (pattern matching). Celem jest filtracja i wyodrębnianie konkretnych danych z obiektu. Dla osób znających programowanie funkcyjne,  nie stanowi to prawdopodobnie wielkiej zmiany. Język C# zawiera coraz więcej elementów programowania funkcyjnego,  przy tym samym zachowując swój styl zaczerpnięty  z CPP i klasycznych języków obiektowych.

W ostatnim poście opisałem tzw. “type pattern” ponieważ wydobywaliśmy dane na podstawie typu. Operator zwracał “true”, gdy typ był zgodny z naszymi oczekiwaniami:

object test = 5;
 
if(test is int integerValue)
{
    Console.WriteLine(integerValue.GetType());
    Console.WriteLine(integerValue * 5);
}

Kolejnym rodzajem wzorca jest “var pattern”:

            object test = 5;

            if (test is var x)
            {
                Console.WriteLine(x.GetType());
            }

Wzorzec “var” zwraca zawsze true. Oznacza to, że instrukcja IF zawsze wykona się. Na ekranie w powyższym przypadku wyświetli się “System.Int32”. Zmienna x jednak jest typu System.Object, zatem nie możemy wykonać na niej bezpośrednio operacji arytmetycznych. Po kompilacji, powyższa konstrukcja wygląda następująco:

                        object test = 5;
			object x = test;
			bool flag = true;
			if (flag)
			{
				Console.WriteLine(x.GetType());
			}

Wiem, że na razie wygląda to bezużytecznie, ale w przyszłym poście zajmiemy się rekurencyjnymi wzorcami i wtedy znajduje to zastosowanie.

Kolejny wzorzec to “wildcard pattern”:

             object test = 5;

            if (test is *)
            {
                Console.WriteLine(test.GetType());
            }

Na ekranie oczywiście znów wyświetli się System.Int32. Analogicznie jak wzorzec var, zawsze zwraca true. Jedyna różnica to fakt, że w przypadku wildcard, nie wydobywamy żadnych danych. Po dekompilacji zobaczymy po prostu:

object test = 5;
Console.WriteLine(test.GetType());

Następny wzorzec to “const pattern”, czyli dopasowywanie stałych. Przydaje się np. w instrukcji switch. Przykład:

            object test = 3;

            switch (test)
            {
                case int integerValue when integerValue <= 5:
                    Console.WriteLine(integerValue );
                    break;

                case int integerValue:
                    Console.WriteLine(integerValue * 5);
                    break;
                case string text:
                    Console.WriteLine(text.ToLower());
                    break;
                case *:
                    Console.WriteLine("All");
                    break;
            }

W powyższym przykładzie, dopasowywanie zostało umieszczone w instrukcjach warunkowych CASE. Normalnie, switch porównuje dane za pomocą operatora ==. W c# 7.0 wspiera on dopasowywanie wzorców i w powyższym kodzie skorzystaliśmy ze wzorca “Type” oraz “wildcard”. Ponadto dodano klauzule “when”, gdzie możemy filtrować dodatkowo wartości. Powyższy kod, pokazuje również, że w pewnych sytuacjach, zarówno “wildcard” jak i “var pattern” są przydatne. Oczywiście nie korzystamy z nich jak mamy pojedynczą instrukcję warunkową, ale w przypadku switch jest już to realny przykład użycia.

Po dekompilacji otrzymamy:

	internal class Program
	{
		private static void Main(string[] args)
		{
			object test = 3;
			object obj = test;
			int? num = obj as int?;
			int integerValue = num.GetValueOrDefault();
			if (num.HasValue && integerValue <= 5)
			{
				Console.WriteLine(integerValue);
			}
			else
			{
				num = (obj as int?);
				int integerValue2 = num.GetValueOrDefault();
				if (num.HasValue)
				{
					Console.WriteLine(integerValue2 * 5);
				}
				else
				{
					string text = obj as string;
					if (text != null)
					{
						Console.WriteLine(text.ToLower());
					}
					else
					{
						Console.WriteLine("All");
					}
				}
			}
		}
	}

W przyszłym poście przyjrzymy się rekurencyjnemu dopasowywaniu, które stanowi agregację przedstawionych wzorców.

c# 7.0 – Operator “Is”

W C# 7.0 operator Is, a konkretniej dopasowywanie typów, zostało ulepszone. We wcześniejszych wersjach, byliśmy zmuszeni do pisania następującego kodu:

object test = 5;

if(test is int)
{
int integerValue = (int)test;

Console.WriteLine(integerValue.GetType());
Console.WriteLine(integerValue * 5);
}

W wersji C#, możliwa jest uproszczona konstrukcja:

            object test = 5;

            if(test is int integerValue)
            {
                Console.WriteLine(integerValue.GetType());
                Console.WriteLine(integerValue * 5);
            }

Jeśli zmienna jest danego typu, automatycznie zostanie zrzutowana. Innymi słowy, za pomocą jednego operatora wykonujemy 3 operacje: sprawdzenie typu, zrzutowanie oraz wykonanie instrukcji warunkowej. W poprzednich wersjach, możliwe było zrobienie czegoś podobnego, ale powstały kod zwykłe był mniej przejrzysty.
Po skompilowaniu, kod jest skonwertowany do postaci z operatorem “as”:

			object test = 5;
			int? num = test as int?;
			int integerValue = num.GetValueOrDefault();
			bool hasValue = num.HasValue;
			if (hasValue)
			{
				Console.WriteLine(integerValue.GetType());
				Console.WriteLine(integerValue * 5);
			}

W następnym wpisie, pokażę kilka kolejnych zmian, które pojawiły się w w obrębie dopasowywania typów.

C# 7.0: Literały binarne

Kolejną ciekawostką z wersji 7.0 są literały binarne. Od dawna w c# możliwe było prezentowanie wartości w systemie szesnastkowym np.:

   int a = 0xF;
   Console.WriteLine(a);

Na ekranie wyświetli się liczba w systemie dziesiętnym czyli 15. Analogicznie w 7.0 możemy napisać kod z liczbami binarnymi:

    int a = 0b1111;
    Console.WriteLine(a);

Powyższy kod we wcześniejszych wersjach nie skompilowałby się. Literały binarne zaczynamy od prefiksu 0b. Dla większości użytkowników jest to raczej ciekawostka, przydatna jedynie w nauce podstawowych algorytmów.

Kolejną ciekawostką, która aczkolwiek może być ciekawsza dla większej grupy programistów, jest możliwość separacji cyfr w literałach. Załóżmy, że mamy bardzo długą stałą, np.:

   int a = 7676242;
   Console.WriteLine(a);

Liczba nie jest zbyt czytelna. Za pomocą znaku podkreślenia, w C# 7.0 możemy:

  int a = 7_676_242;
  Console.WriteLine(a);

Oczywiście można korzystać z dowolnej liczby podkreśleń, np.:

int a = 7______676______242;
Console.WriteLine(a);

Wygląda to bardziej przejrzyście. Nic wielkiego, ale czasami przydatne.

C# 7.0 – lokalne funkcje

Dzisiaj zaczynam pierwszy wpis o nowościach w C# 7.0. Przede wszystkim warto ściągnąć Visual Studio 15 Preview. Wersja 15, to przyszły następca Visual Studio 2015, który określany był wersją 14.

Jeśli chcemy sprawdzić nowości z C# 7.0 musimy najpierw ustawić  __DEMO__ oraz  __DEMO_EXPERIMENTAL__  we właściwościach projektu:

1

Pierwsza nowość to zagnieżdżone funkcję lokalne. Przykład:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {

                return a;
            }

            Console.WriteLine(GetValue(5));
        }
    }

Innymi słowy jest to funkcja w funkcji. Czasami deklarujemy prywatną metodę, która używana jest jedynie w jednej metodzie jako helper. Za pomocą lokalnej funkcji w łatwy sposób możemy ograniczyć jej zasięg.

Funkcja jest lokalna, zatem poniższy kod nie skompiluje się:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {

                return a;
            }

            Console.WriteLine(GetValue(5));
        }

        public void DoSomething1()
        {
            Console.WriteLine(GetValue(5));
        }
    }

Lokalną funkcję można tylko wywoływać w metodzie, w której ją zadeklarowano. Analogicznie poniższy kod wywoła błąd kompilacji:

    class Program
    {
        private static void Main(string[] args)
        {
            Test test = new Test();
            test.DoSomething();
            test.GetValue(5);
        }
    }

Co prawda można wywołać test.DoSomething, ale test.GetValue już nie.
Możliwe jest również deklarowanie funkcji zagnieżdżonych wielokrotnie, tzn.:

   public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {
                string GetString(string b)
                {
                    return b;
                }

                Console.WriteLine(GetString(a.ToString()+" as text"));

                return a;
            }

            Console.WriteLine(GetValue(5));
        }
    }

Na ekranie najpierw wyświetli się “5 as text”, a potem 5. Z kolei następujący kod już nie skompiluje się:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {
                string GetString(string b)
                {
                    return b;
                }                

                return a;
            }

            Console.WriteLine(GetString(a.ToString() + " as text"));
        }
    }

Oznacza to, że z funkcji A można jedynie odnosić się do tej najbardziej zewnętrznej funkcji lokalnej. Nic z kolei nie stoi na przeszkodzie, aby zadeklarować kilka metod o tym samym stopniu zagnieżdżenia:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {              
                return a;
            }

            string GetString(string b)
            {
                return b;
            }


            Console.WriteLine(GetString("5 as text"));
            Console.WriteLine(GetValue(5));
        }
    }

Jakiekolwiek modyfikatory są zabronione, np. taki kod nie skompiluje się:


    public class Test
    {
        public void DoSomething()
        {
            static int GetValue(int a)
            {              
                return a;
            }
            Console.WriteLine(GetValue(5));
        }
    }

Analogicznie sprawa wygląda z public czy nawet private.
Możliwe jest z kolei z korzystanie z metod wyrażonych w formie lambda (nowość z c# 6.0):

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a) => a;
      
            Console.WriteLine(GetValue(5));
        }
    }

Funkcja lokalna może mieć również dostęp do zmiennych zadeklarowanych w zewnętrznej funkcji, tzn.:

       public class Test
    {
        public void DoSomething()
        {
            int outerValue = 43;

            void Nested()
            {
                Console.WriteLine(outerValue);
                outerValue = 100;
            }

            Nested();
            Console.WriteLine(outerValue);
        }
    }

Na ekranie wyświetli się 43, a potem 100, ponieważ wartości kopiowane są w analogiczny sposób jak to ma miejsce w anonimowych funkcjach. Kolejne zagnieżdżenia mogą korzystać ze zmiennych zadeklarowanych w nadrzędnych funkcjach.

Na zakończenie warto spojrzeć co naprawdę jest wygenerowane w tle:

.class /*02000003*/ public auto ansi beforefieldinit 
  ConsoleApplication1.Test
    extends [mscorlib]System.Object
{

  .method /*06000003*/ public hidebysig instance void 
    DoSomething() cil managed 
  {
    .maxstack 8

    // [22 9 - 22 10]
    IL_0000: nop          
    IL_0001: nop          

    // [27 13 - 27 44]
    IL_0002: ldc.i4.5     
    IL_0003: call         int32 ConsoleApplication1.Test::'<DoSomething>g__GetValue0_0'(int32)
    IL_0008: call         void [mscorlib]System.Console::WriteLine(int32)
    IL_000d: nop          

    // [28 9 - 28 10]
    IL_000e: ret          

  } // end of method Test::DoSomething

  .method /*06000004*/ public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [mscorlib]System.Object::.ctor()
    IL_0006: nop          
    IL_0007: ret          

  } // end of method Test::.ctor

  .method /*06000005*/ assembly hidebysig static int32 
    '<DoSomething>g__GetValue0_0'(
      /*08000002*/ int32 a
    ) cil managed 
  {
    .custom /*0C00000F*/ instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() /*06003F5F*/ 
      = (01 00 00 00 )
    .maxstack 1
    .locals init (
      [0] int32 V_0
    )

    // [24 13 - 24 14]
    IL_0000: nop          

    // [25 17 - 25 26]
    IL_0001: ldarg.0      // a
    IL_0002: stloc.0      // V_0
    IL_0003: br.s         IL_0005

    // [26 13 - 26 14]
    IL_0005: ldloc.0      // V_0
    IL_0006: ret          

  } // end of method Test::'<DoSomething>g__GetValue0_0'
} // end of class ConsoleApplication1.Tes

Warto zwrócić uwagę na dwie sygnatury:

 .method /*06000003*/ public hidebysig instance void 
    DoSomething() cil managed 
[/csharp

oraz


 .method /*06000005*/ assembly hidebysig static int32 
    '<DoSomething>g__GetValue0_0'(
      /*08000002*/ int32 a
    ) cil managed 

.

Innymi słowy, w praktyce dwie różne metody są generowane (lokalna funkcja to ta prywatna).

C# 6.0 – podsumowanie

Od następnego wpisu będzie już o C# 7.0. Wersję 6.0 niejednokrotnie już opisywałem, włączając wersje community review. Pomyślałem zatem, że warto napisać jeden wpis agregujący nowości z C# 6.0, zanim przejdziemy do 7.0.

BenchmarkDotNet – prosta biblioteka do testów wydajnościowych

Dzisiaj chciałbym pokazać BenchmarkDotNet. Dzięki niemu w łatwy sposób można przetestować wydajność konkretnych metod w c#. Na blogu temat wydajności poruszałem już wiele razy i wiemy,  nie jest łatwe prawidłowe zmierzenie czasu wykonania kodu. Pamiętajmy, że kod wykonany pierwszy raz zawsze musi zostać przetłumaczony do kodu maszynowego (JIT). W momencie wywołania pierwszy raz jakiejkolwiek metody, CLR sprawdzi czy dana metoda ma już kod maszynowy. Jeśli jakaś metoda w ogóle nie została wykonana, wtedy nie ma nawet potrzeby generowania kodu maszynowego. W celu rzetelnego przetestowania jakiejkolwiek metody, należy zawsze wykonać “warm-up”.

Dzięki BenchmarkDotNet nie musimy się o to martwić. Automatycznie zostaną stworzone odizolowane projekty dla każdego z testów. Framework zadba o wyliczenie statystyk takich jak m.in. mediana czy odchylenie standardowe.

Zacznijmy od instalacji pakietu NuGet:

PM> Install-Package BenchmarkDotNet

Kod, który będziemy testować wygląda następująco:

    public class SomeCode
    {
        public void CalculateSlow()
        {
            Thread.Sleep(5000);
        }

        public void CalculateFast()
        {
            Thread.Sleep(5);
        }
    }

Sam test z kolei, to nic innego jak klasa oznaczona atrybutami [Benchmark]:

    public class PerformanceTests
    {
        private readonly SomeCode _sut=new SomeCode();

        [Benchmark]
        public void CalculateSlowTest()
        {
            _sut.CalculateSlow();
        }

        [Benchmark]
        public void CalculateFastTest()
        {
            _sut.CalculateFast();
        }
    }

W celu wykonania testu, należy skorzystać z klasy BenchmarkRunner:

Summary summary = BenchmarkRunner.Run<PerformanceTests>();

Jak widzimy, w wyniku dostaniemy obiekt Summary, zawierający szczegóły z testów. Nie musimy się jednak nim przejmować teraz, ponieważ framework wygeneruje automatycznie raporty zarówno w formie plików tekstowych, jak i na wyjściu aplikacji konsolowej. Po odpaleniu powyższego kodu, na początku zobaczymy:
1

Przez następne kilka sekund\minut będą wykonywane powyższe testy. Po zakończeniu, na ekranie konsoli zobaczymy podsumowanie:
2

Oprócz tego, w folderze bin zobaczymy kilka raportów m.in. PerformanceTests-measurements.csv, PerformanceTests-report.csv, PerformanceTests-report.html jak i również w formie markdown. Jeśli nie wystarczają one, można napisać własne i odpowiednio skonfigurować BenchmarkDotNet.

LINQ: GroupJoin

Dzisiaj podstawy, ale wcześniej nie miałem potrzeby skorzystania z  funkcji GroupJoin. Myślę, że prosty przykład jest najlepszą dokumentacją.

Dosyć częstą używaną funkcją jest GroupBy. Jeśli mamy np. listę zamówień w postaci (IdCustomer, Name), wykonując GroupBy na IdCustomer otrzymamy słownik, gdzie kluczem jest identyfikator zamówienia, a wartością lista zamówień danego klienta.

GroupJoin, jak sama nazwa sugeruje jest połączeniem Join z GroupBy. Załóżmy, że mamy następujące encje:

    class CustomerInfo
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    class Order
    {
        public int IdCustomer { get; set; }
        public string Name { get; set; }
    }

Następnie przykładowe dane, definiujące relacje wyglądają następująco:

            var customers = new CustomerInfo[3];
            customers[0] = new CustomerInfo() { Id = 1, Name = "Piotr" };
            customers[1] = new CustomerInfo() { Id = 2, Name = "Pawel" };
            customers[2] = new CustomerInfo() { Id = 3, Name = "tertert" };

            var orderList = new Order[5];
            orderList[0] = new Order() { IdCustomer = 1, Name = "Zamowienie 1" };
            orderList[1] = new Order() { IdCustomer = 1, Name = "Zamowienie 1 a" };
            orderList[2] = new Order() { IdCustomer = 2, Name = "Zamowienie 2 a" };
            orderList[3] = new Order() { IdCustomer = 2, Name = "Zamowienie 2 b" };
            orderList[4] = new Order() { IdCustomer = 3, Name = "Zamowienie 3" };

W celu uzyskania zamówień dla konkretnego klienta wystarczy:

            var customerOrders = customers.GroupJoin(orderList, x => x.Id, x => x.IdCustomer, (customer, orders) => new
            {
                CustomerName = customer.Name,
                Orders = orders.ToArray()
            });

            foreach (var customerOrder in customerOrders)
            {
                Console.WriteLine(customerOrder.CustomerName);

                foreach (var order in customerOrder.Orders)
                {
                    Console.WriteLine("\t{0}", order.Name);
                }
            }

Wynikiem będzie lista klientów wraz z zamówieniami:
1

Definiowanie własnych typów danych w C# (statyczne typowanie)

Zwykle programiści korzystają z podstawowych typów dostarczonych przez C#, takich jak String, Int32 czy Double. W świecie programowania obiektowego można jednak pójść o krok dalej i budować własne typy danych. Przeważnie programiści korzystają z nich wyłącznie, gdy do zaimplementowania jest jakaś logika. Dlaczego nie tworzyć ich nawet w sytuacjach, gdy mają one przechowywać wyłącznie dane?

Problem z podstawowymi typami takimi jak String Czy Int32 to fakt, że stanowią one wyłącznie fizyczną, a nie logiczną reprezentację danych. Int32 opisuje każdą liczbę całkowitą, co jest dość szerokim określeniem. Załóżmy, że w kodzie mamy dwie jednostki do opisania:
– prędkość (km/h)
– temperatura (C)

Standardowe rozwiązanie to użycie typów podstawowych, np. Int32:

int velocity = 85;
int temperature = 20;

Niestety powyższy kod nie będzie mógł korzystać z zalet statycznego typowania. Poniższy kod skompiluje się i o błędzie dowiemy się dopiero przy wykonywaniu jakiś operacji:

int velocity = 85;
int temperature = 20;

int result = velocity * temperature;

Prawdopodobnie nie ma sensu mnożyć prędkości przez temperaturę. Analogicznie typy podstawowe nie dostarczają żadnej walidacji logicznej reprezentacji. Co prawda w przypadku liczb, możemy zadeklarować velocity jako uint co ma większy sens ponieważ unikniemy wtedy błędów z związanych z ujemną prędkością. Problem jednak pojawi się w przypadku string:

string email="test@test.com";

W tym przypadku zdecydowanie potrzebna jest walidacja.

Zdefiniujmy zatem typy opisujące logiczną reprezentację danych:

    struct Velocity
    {
        private readonly uint _value;

        public Velocity(uint value)
        {
            _value = value;
        }
    }

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {
            _value = value;
        }
    }

Od tej pory, niemożliwe jest popełnienie następującego błędu:

var velocity = new Velocity(43);
var temperature = new Temperature(5);

var result = velocity + temperature;

Kod zakończy się błędem kompilacji: “Operator ‘+’ cannot be applied to operands of type ‘Velocity’ and ‘Temperature'”. Błąd wykryty na etapie kompilacji jest oczywiście dużo bardziej pożądany niż te spotykane już w trakcie działania aplikacji.

Oprócz korzyści z związanych z bezpiecznym typowaniem, kod jest łatwiejszy w zrozumieniu. Typ nadaje kontekst danej wartości.
Dodanie walidacji sprowadza się teraz wyłącznie do:

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {        
            _value = value;

            if (!IsValid(value))
                throw new ArgumentException("Temperature must be between -2000 and 2000.");
        }

        private bool IsValid(int value)
        {
            return value < 2000 && value > -2000;
        }
    }

Oczywiście to dopiero początek. W praktyce chcemy mieć do dyspozycji pewne operatory, np.:

            var temperature1 = new Temperature(42);
            var temperature2 = new Temperature(42);
            var temperature3 = new Temperature(41);

            Console.WriteLine(temperature1 == temperature2); // true
            Console.WriteLine(temperature1 == temperature3); // false

Jeśli chcemy wspierać ==, wtedy wystarczy napisać:

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {        
            _value = value;

            if (!IsValid(value))
                throw new ArgumentException("Temperature must be between -2000 and 2000.");
        }

        private bool IsValid(int value)
        {
            return value < 2000 && value > -2000;
        }

        public static bool operator ==(Temperature t1, Temperature t2)
        {
            return t1.Equals(t2);
        }

        public static bool operator !=(Temperature t1, Temperature t2)
        {
            return !t1.Equals(t2);
        }
    }

W praktyce warto zadeklarować bazowy typ, który implementuje najczęściej używane operacje (==,!=,>,<,+,-). Nie można również przesadać w drugą stronę i każdą zmienną owijać w typ. Moim zdaniem bardzo szybko taki kod stanie się przykładem overengineering. Jeśli jakiś typ danych występuje bardzo często w kodzie (np. money), wtedy warto o tym pomyśleć.