.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.

Visual Studio 15 Preview: Wykonywanie kodu w oknie Interactive.

O oknie “Interactive” pisałem już tutaj. Bardzo pożyteczna funkcja, w moim przypadku zastępująca LinqPad.

W wersji 15 Preview (którą można pobrać z stąd), dodano możliwość wykonywania zaznaczonego kodu. Załóżmy, że funkcja Main wygląda następująco:

1

Możliwe jest teraz zaznaczenie kodu i wykonanie go w oknie Interactive C#:

2

Warto zwrócić uwagę również na skrót – Ctrl+E. To bardzo pożyteczne, gdy chcemy szybo coś przetestować. Okno otworzy się automatycznie i zostanie wykonany zaznaczony kod:

3

Niby nic wielkiego, ale wiele razy musiałem otwierać LinqPad, albo w przypadku wersji 2015 ręcznie okno Interactive.

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

1
 .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.

Wirtualne maszyny a kontenery

Za sprawą Docker, coraz bardziej popularnym terminem staje się “software container”. Jak to bywa z nowikami ostatnio, czasami są one wdrażane na siłę, w środowiskach gdzie nie ma takiej potrzeby.

Na blogu skupiam się na środowisku Windows, dlatego również o kontenerach będę pisał od strony programistów Windows. Najpierw jednak wypada wyjaśnić co to jest kontener?

Wiele lat temu, standardem były fizyczne maszyny. Gwarantowało to oczywiście 100% izolacji między aplikacjami. Aplikacja na komputerze A, nie mogła bezpośrednio adresować pamięci na komputerze B. Z punktu widzenia bezpieczeństwa osiągano w ten sposób wysoką niezależność. Niestety, w momencie gdy chcieliśmy wdrążyć drugi serwis\aplikację, wtedy stawało się to bardzo kosztowne ponieważ musieliśmy zakupić dodatkowy serwer. Można to pokazać następująco:

1

Ogromnym przełomem były wirtualne maszyny. Dzięki nim, na tym samym komputerze, mogliśmy symulować kilka innych systemów operacyjnych. Co ważne, każdy z nich miał swoje zasoby takie jak CPU czy pamieć podręczna. Z punktu widzenia izolacji i bezpieczeństwa, rozwiązanie było znakomite.

2

Nastały jednak czasy, gdzie taka skalowalność nie wystarczała. Wyobraźmy sobie architekturę opartą o mikro-serwisy. Przy dużym ruchu chcemy mieć możliwość aktywowania pewnych węzłów jak szybko tylko to możliwe. Wirtualne maszyny to dosyć ciężkie rozwiązanie. Każda maszyna to osobny system operacyjny. Każdy system z kolei pożera na starcie masę zasobów (CPU, pamieć). Instalacja maszyny również jest dość powolna.

Z pomocą przychodzą kontenery. Można je określić mianem wewnętrznej wirtualizacji. Pojedynczy system zawiera kilka kontenerów, które poziomem izolacji przypominają wirtualne maszyny:

3

Dzięki kontenerom nie mamy overhead, który wiązał się z wirtualną maszyną. Uruchomienie nowego kontenera jest również dużo szybsze niż VM. W wielu wypadkach takie rozwiązanie jest optymalne ponieważ zwykle nie potrzebujemy osobnego systemu, a chodzi nam wyłącznie o izolację. W przypadku mikro-serwisów, kontenery są dobrym rozwiązaniem ponieważ nie potrzebujemy osobnych systemów operacyjnych – wystarczy jeden, wspierający kontenery.

Parameters sniffing – SQL Server

Kilka ostatnich wpisów poświęciłem na temat SQL Server. Ostatnio pisałem o SQL Statistics, które wpływa na wygenerowanie planu wykonania. Jak wiemy, plan jest zawsze buforowany, co wpływa zwykle pozytywnie na wydajność. Analiza zapytania i wygenerowanie planu są dość czasochłonne, dlatego zwykle plan jest zapisywany.

Problem w tym, że nie zawsze plan przechowywany w cache jest najbardziej optymalny. Zwykle pierwsze wykonanie danego zapytania powoduje zapis planu w pamięci. Co jeśli kolejne zapytania będą używały innych parametrów, a co za tym idzie, inny plan może być skuteczniejszy?

Problem pokażę, na przykładzie bazy AdventureWorks, którą można ściągnąć z stąd.
Do dyspozycji mamy tam tabelę Address.

Zapytanie:

SELECT* FROM [AdventureWorks2014].[Person].[Address]

Wynik:

1

Jeden z indeksów pokrywa kolumnę StateProvinceID. Wykonajmy poniższe zapytanie, aby dowiedzieć się trochę więcej o dystrybucji danych:

SELECT StateProvinceID,count(*)
  FROM [AdventureWorks2014].[Person].[Address] group by StateProvinceID order by count(*) desc

Wynik:

2

Widzimy, że bardzo dużo wierszy ma StateProvinceId równy 9. Z kolei 119 ma tylko jedno wystąpienie. Wykonajmy zatem  poniższe zapytania na tej kolumnie, aby zobaczyć wygenerowane plany wykonania:

 SELECT * FROM [AdventureWorks2014].[Person].[Address] where StateProvinceID=9
 GO
 SELECT * FROM [AdventureWorks2014].[Person].[Address] where StateProvinceID=119

Plany wykonania:

3

Widzimy, że pierwsze zapytanie nie korzysta z indeksu, ponieważ jest mało selektywne i LookUp kosztował by więcej niż skanowanie. Drugie zapytanie, które zwraca wyłącznie jeden wiersz jest dużo lepszym kandydatem na przeszukiwanie indeksu.
Plan nie został zbuforowany bo to fizycznie dwa różne zapytanie.

Stwórzmy teraz procedurę, która wykonuje analogiczne zapytanie:

CREATE PROCEDURE GetAddressByStateId
	@stateId int
AS
BEGIN
  
    SELECT * FROM [AdventureWorks2014].[Person].[Address] where StateProvinceID=@stateId
END
GO

Wykonajmy również procedurę przekazując dwa różne identyfikatory stanów:

  exec GetAddressByStateId 9
  GO
  exec GetAddressByStateId 119

Plany wykonania:

4

Co się stało? Pierwsze wykonanie zbuforowało plan wykonania. Drugie wywołanie zatem nie będzie próbowało znaleźć optymalnego planu, a skorzysta z tego zawartego w cache. Problem w tym, że dla skomplikowanych procedur nie zawsze pierwszy plan wykonania jest najlepszy.

Jednym z “obejść” jest rekompilacja procedury za każdym razem za pomocą WITH RECOMPILE:

ALTER PROCEDURE GetAddressByStateId
	@stateId int  WITH RECOMPILE
AS
BEGIN
  
    SELECT * FROM [AdventureWorks2014].[Person].[Address] where StateProvinceID=@stateId
END
GO

Innym rozwiązaniem może być skorzystanie z Query Hints. Za pomocą wskazówek można ułatwić SQL Server wygenerowanie najlepszego planu. W praktyce jednak, jeśli mamy problemy z parameter sniffing oznacza to, że procedura jest zbyt skomplikowana i część logiki powinna być wykonywana np. w osobnej funkcji.