wtorek, 12 lutego 2013

70-513 Wzorce wymiany komunikatów w WCF

Wymiana komunikatów w WCF opisana jest za pomocą protokołu MEP.

Request/Response
Najczęściej używany, gdyż nie wymaga praktycznie żadnej konfiguracji.
Wymagania co do tego typu komunikacji:
  • IsOneWay - false
  • brak duplexu
Wszystkie bindingi za wyjątkiem MSMQ obsługują ten rodzaj wymiany komunikatów.


OneWay
W tym przypadku ustawiamy wartość właściwości OneWay na true. W przypadku tego rodzaju wymiany, klient wysyła zapytanie do serwisu i nie oczekuje na żadną odpowiedź. Aspekty które należy rozważyć w przypadku tego rodzaju połączenia:
  • brak możliwości używania kontraktów typu Fault - serwis nie zwróci niczego, więc też nie powiadomi o występujących problemach
  • poprzedni punkt implikuje, iż nie jest to do końca bezpieczna metoda w przypadku gdy wysyłamy ważne dane. Nie mamy informacji zwrotnej czy dane zostały poprawnie zapisane np. do bazy danych
  • wywołanie w taki sposób akcji, może błędnie tworzyć obraz, iż akcja zostanie wykonana asynchronicznie. Niestety tak nie jest i należy pamiętać iż klient i tak zostanie zablokowany na czas wykonania akcji.

Duplex
Ten rodzaj komunikacji może być przydatny, gdy po wysłaniu wiadomości do serwera chcemy otrzymać od niego wiadomość zwrotną, nie koniecznie blokując wykonanie całej aplikacji. Oczywiście serwis może wysyłać do klienta także nie zamówione wiadomości.
Aby serwis był duplexowy należy w atrybucie ServiceContract uzupełnić właściwość CallbackContract wskazując interfejs pełniący rolę metody zwrotnej.
W przypadku tego rodzaju komunikacji należy ustanowić sesję pomiędzy klientem a serwisem za pomocą właściwości InstanceContextMode.
Sposób ten posiada szereg wad:
  • klient musi posiadać stały adres ip
  • brak skalowalności; konieczność zarządzania sesją; powrót do ery klient-serwer? 
  • nie wykorzystamy w przypadku innych platform np. Java
  • problemy z wielowątkowością 

poniedziałek, 11 lutego 2013

70-513 Kontrakty

Egzamin 70-513 dotyczy technologi WCF - Windows Communication Foundation. Technologia ta umożliwia deweloperowi budować aplikacje zorientowane na usługi.

Pierwszym, ważnym aspektem serwisu stworzonego w WCF jest zdefiniowanie kontraktów. Określają one między innymi format wysyłanej wiadomości, odpowiedź na nią. Odpowiadają także na pytanie co w razie błędów. Definiujemy trzy rodzaje kontraktów, które zostaną kolejno omówione.


Behavioral Contracts
Behavioral Contracts określają w jaki sposób zachowuje się serwis. Podstawowym atrybutem nakładanym w tej kategorii jest ServiceContract. Atrybut ten dodajemy do klasy bądź interfejsu - ze szczególnym naciskiem na interfejs (jest to bardziej eleganckie rozwiązanie). Atrybut ten posiada szereg właściwości:
  • Name - nazwa kontraktu - domyślnie jest to nazwa interfejsu bądź klasy. Nazwa ta jest później używana w polu portType aplikacji, która konsumuje serwis
  • Namespace - przestrzeń nazw dla definicji WSDL
  • CallbackContract - interfejs biorący udział w przypadku komunikacji dwustronnej (duplex)
  • ProtectionLevel - decyduje o tym w jaki sposób są chronione wiadomości (szyfrowanie, podpis cyfrowy)
  •  ConfigurationName - nazwa konfiguracji w pliku konfiguracyjnym (web.config/app.config)
  • SessionMode - konfiguracja sesji
Kolejnym atrybutem który możemy zastosować to OperationContract. Atrybut ten służy m.in. do definiowania sposobu ekspozycji metod, formatu wiadomości. Niektóre możliwe parametry do ustawienia:
  • Name - nazwa operacji - domyślnie nazwa metody
  • Action - nagłówek akcji
  • ReplyAction - nagłówek odpowiedzi
  • IsOneWay - definiuje czy akcja jest tylko w jedną stornię - nie jest wtedy brany pod uwagę nagłówek zdefiniowany w ReplyAction 
  •  ProtectionLevel - szyfrowanie dla pojedynczej metody
  •  IsInitiating - definiuje czy wywołanie tej metody tworzy nową sesję pomiędzy wywołującym a serwisem
  • IsTerminating - definiuje czy wywołanie tej metody kończy istniejącą sesję
W przypadku Action można użyć jako nazwy "*", co będzie oznaczą, że w przypadku podania błędnej nazwy zostanie wywołana metoda oznaczona gwiazdką:

Code:
        [OperationContract(Action = "*")]
        void DoSomething(Message message);

Kolejnym możliwym atrybutem do nadania jest MessageParameter. Określa on w jaki sposób nazwy zmiennych przekazywanych do operacji są nazwane w XMLu powstałym w wyniku serializacji obiektów.
Przykład:

Code:
        [OperationContract()]
        [return: MessageParameter(Name = "responseString")]
        string SomeOp([MessageParameter(Name = "string")]string s);



Fault Contracts
Kontrakt tego typu opisuje co w przypadku błędów w serwisie zostanie wysłane do klienta. Należy sobie uświadomić, że Fault to nie jest to samo co Exception.
Exception to główny sposób powiadamiania w .NET o błędach podczas wykonywania aplikacji. Fault to wiadomość o błędzie w kontekście protokołu SOAP.
WCF posiada klasę FaultException, którą można traktować jako obiekt Proxy pomiędzy światem web serwisem a platformą .NET. Klasa FualtException posiada dwie wersje:
  • FaultException - nietypowany wyjątek
  • FaultException<TDetail> - generyczna wersja wyjątku
Najważniejsze fakty nt. FaultException:
  • atrybut można nałożyć tylko na operacje
  • atrybut ten nie jest dziedziczony
  • może być nałożony na operację kilka razy (np. w celu zaznaczenia iż metoda może rzucić kilka różnych rodzajów wyjątków) 
  • konstruktor przyjmuje typ wyjątku 
 Przykład implementacji:

Code:
//IService1
        [OperationContract]
        [FaultContract(typeof (string))]
        string GetSomeData(int id);

//Service1
        public string GetSomeData(int id)
        {
            if (id < 1)
            {
                throw new FaultException<string>("Id can't be less than 1");
            }

            return "Ok";
        }

niedziela, 10 lutego 2013

70-516 LINQ

O LINQu pisałem w wielu postach. Nie zamierzam więc rozpisywać się o nim, tylko zbiorę najważniejsze fakty i przykłady.

LINQ można użyć dla każdej kolekcji która implementuje IEnumerable lub generyczny IEnumerable<T>.

Zapytania LINQ można pisać w wersji SQLo podobnej. Różnicą jest to, iż rozpoczynamy od klauzuli FROM. Dalczego? Jeżeli nie zaczynalibyśmy od FROM, czyli wskazania źródła do którego się odwołujemy nie mielibyśmy wsparcia IntelliSense.

Opóźnione wykonanie zapytania - definicja LINQ jest traktowana jako zapytanie, które zostanie wykonane dopiero kiedy wynik będzie faktycznie potrzebny. Właściwość ta ma tę konsekwencję, że każde wywołanie zapytania będzie się odwoływało do źródła danych. Jeżeli chcemy przechować dane lokalnie i nie odwoływać się do źródła wielokrotnie, należy dane umieścić w kolekcji (np. Liście)


Cechy które składają się na LINQ:
  • inicjalizacja obiektów
  • niejawne typowanie - var; tylko jako zmienne lokalne; w deklaracji musi nastąpić ich inicjalizacja - nie może być to null;
  • typy anonimowe
  • wyrażenia lambda
  • metody rozszerzające - extension methods
Metody rozszerzające zawarte w interfejsie Enumerable:
  • All - sprawdza elementy kolekcji i zwraca prawdę jeżeli wszystkie elementy spełniają podane warunki
  • Any - sprawdza elementy kolekcji i zwraca prawdę jeżeli przynajmniej jeden element spełnia podane warunki
  • AsEnumerable - konwertuje dane do IEnumerable (np. z IQueryable)
  • AsQueryable - konwertuje do interfejsu IQueryble (np. z IEnumerable)
  • Average - średnia elementów
  • Cast - rzutuje elementy kolekcji na podany typ
  • Concat - pozwala łączyć ze sobą kolekcje, przykład:
    Code:
        class Program
        {
            static void Main(string[] args)
            {
                var personRepository = new PersonRepository();
                var collection1 = personRepository.GetPersonList1();
                var collection2 = personRepository.GetPersonList2();
                var combindedCollection = collection1.Concat(collection2).Concat(collection1).ToList();
            }
        }
    
        class Person
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        class PersonRepository
        {
            public IList<Person> GetPersonList1()
            {
                return new List<Person>
                           {
                               new Person {FirstName = "Jacek", LastName = "Kowalski"},
                               new Person {FirstName = "Marek", LastName = "Jackowski"}
                           };
            }
    
            public IList<Person> GetPersonList2()
            {
                return new List<Person>
                           {
                               new Person {FirstName = "Sebastian", LastName = "Kowalski"},
                               new Person {FirstName = "Seweryn", LastName = "Opolski"}
                           };
            }


  • Contains - sprawdza czy podany element znajduje się w kolekcji
  • Count - ilość elementów w kolekcji
  • DefaultIfEmpty - w przypadku gdy kolekcja nie zawiera żadnych elementów, zwraca jeden o domyślnej wartości (false, 0, null)
  • Distinct - usuwa duplikaty ze źródła
  • ElementAt(i) - zwraca element na danej pozycji w źródle
  • ElementAtOrDefault - jak wyżej z tą różnicą, że w przypadku nie znalezienia elementu nie zostanie rzucony wyjątek tylko zwrócona wartość domyślna
  • Except - zwraca elementy zawarte w pierwszej kolekcji a nieobecne w drugiej
  • First oraz FirstOrDefault - zwraca pierwszy element kolekcji
  • GroupBy - grupuje elementy według zadanego klucza, przykład:
    Code:
                var adventureWorksLtContext = new AdventureWorksLTContext();
                var customers = adventureWorksLtContext.Customers.ToList();
                IEnumerable<IGrouping<string, Customer>> customersGroupByFirstName = customers.GroupBy(x => x.FirstName);
                foreach (var group in customersGroupByFirstName)
                {
                    foreach (var customer in group)
                    {
                        Console.WriteLine("{0} {1}", customer.FirstName, customer.LastName);
                    }
                }


  • GroupJoin - zapytanie podobne do SQLowego Left Outer Join
  • Intersect - przecięcie zbiorów
  • Join - łączy dwa zbiory na zasadzie równości kluczy
  • Last oraz LastOrDefault - zwraca ostatni element kolekcji
  • LongCount - to samo co Count, tylko zwraca rezultat jako typ long
  • Max - zwraca największy element kolekcji
  • OfType<T> - zwraca elementy, które są typu T
  • Min - najmniejszy element kolekcji
  • OrderBy, OrderByDescending, ThenBy, ThenByDescending - sortowanie
  • Reverse - odwraca kolejność elementów w kolekcji
  • Select - projekcja rezultatu
  • SelectMany - jeden element wejściowy tworzy wiele wyjściowych. Szczególnie przydatny w przypadku pracy nad kolekcjami kolekcji - łączy wtedy kolekcje z obiektów w jedną wynikową
  • SequenceEqual - sprawdza czy obie kolekcje zawierają te same elementy, w tej samej kolejności
  • Single oraz SingleOrDefault - zwraca jeden element z kolekcji - jeżeli jest więcej elementów w kolekcji zostanie rzucony wyjątek, bądź zostanie zwrócona wartość domyślna
  • Skip - przeskakuje zadaną ilość elementów
  • SkipWhile - przeskakuje zadaną ilość elementów dopóki dany warunek jest spełniony
  • Sum - zwraca sumę 
  • Take - pobiera zadaną ilość elementów z kolekcji 
  • TakeWhile - to samo co Take, ale pobiera elementy spełniające warunek 
  • ToArray - konwertuje kolekcję na tablicę
  • ToDictionary - konwertuje kolekcję na słownik
  • ToList - konwertuje kolekcję do listy
  • ToLookUp - tworzy kolekcję grup
  • Union - łączy kolekcje, usuwa elementy powtarzające się
  • Where - filtruje elementy
  • Zip - łączy dwie kolekcje na podstawie zadanego warunku; liczba elementów jest równa mniejszej kolekcji

Zastosowania LINQ:
1. Stronicowanie:
Łączymy metody Skip i Take:
kolekcja.Skip(nr_strony * ilość_elementów_na_stronie).Take(ilość_elementów_na_stronie)

2. Złączenie równościowe - Inner Join

Code:
from a in collectionA
join b in collectionB
on a.Id equals b.IdA

3. Złączenia zewnętrzne - Outer Join

Code:
from a in collectionA
join b in collectionB
on a.Id equals b.IdA into c
from d in c.DefaultIfEmpty()

3. Iloczyn kartezjański - Cross Join

Code:
from a in collectionA
from b in collectionB


PLINQ - Parallel LINQ
PLINQ pozwala na użycie dodatkowych wątków w celu szybszego wykonania zadanego zadania. Aby uaktywnić wielowątkowe wykonywanie zapytania należy skorzystać z metody rozszerzającej AsParallel. Wykonane zachowanie nie zachowa kolejności wyników. Jeżeli chcemy aby kolejność została zachowana należy skorzystać z metody AsOrdered.

Zapis danych z RadGridView do CSV

Bardzo często chcemy zapisać dane do pliku w formacie CSV. Format ten jest na tyle prosty, że wiele aplikacji umożliwia jego import. Nadaje się więc idealnie do przenoszenia danych pomiędzy aplikacjami.
Telerik w kontrolce RadGridView oferuje dwie metody zapisu danych do formatu CSV:


Code:
            string content = radGridView.ToCsv(true);
            using (var sw = new StreamWriter("data.csv"))
            {
                sw.Write(content);
            }

Sposób ten wykorzystuje metodę rozszerzającą ToCsv. Parametr przekazany do tej metody wskazuje czy nagłówek (nazwy kolumn) mają także zostać dodane do zawartości pliku.

Drugi sposób, umożliwiający między innymi ustawienie:
  • kultury
  • kodowania znaków
  • zapis nagłówków kolumn
  • stopki

Code:
            var gridViewExportOptions = new GridViewExportOptions();
            gridViewExportOptions.Format = ExportFormat.Csv;
            radGridView.Export(new FileStream("data.csv", FileMode.Create), gridViewExportOptions);

Kod przykładu do pobrania: http://sdrv.ms/WXAs5I

czwartek, 7 lutego 2013

Wrażenia po egzaminie 70-516

Egzamin już za mną więc przyszedł czas na krótkie podsumowanie.
70-516 dotyczy dostępu do źródeł danych. Głównym elementem, na który został położony największy nacisk w pytaniach to Entity Framework w wersji Database-First. Obecnie wydaje mi się osobiście, że to rozwiązanie Code First przeważa jeżeli chodzi o nowe projekty - to jednak tylko moja prywatna opinia na ten temat.
Pierwszych pięć pytań na egzaminie dotyczyło realnego schematu bazy, gdzie zostały podane dane skryptów, mapowań plików EF, kodu procedur SQL itp.
Kolejne pytania dotyczyły już ogólnie wiedzy na temat dostępu do danych.
Ogólnie poziom pytań był bardzo zróżnicowany - od bardzo prostych, do takich w których trzeba było się zastanowić głębiej nad odpowiedzią. 

Kończąc wywód - dwa MCTS-y w moich rękach. Czas na kolejny :)

poniedziałek, 4 lutego 2013

70-516 Szyfrowanie danych

1. Szyfrowanie symetryczne
 

Szyfrowanie symetryczne opiera się na jednym kluczu użytym zarówno do szyfrowania jak i deszyfrowania danych. Obecnie zalecane jest używanie algorytmu Rijndael - dokładny opis można znaleźć w wikipedii: http://en.wikipedia.org/wiki/Advanced_Encryption_Standard


Code:
    class Program
    {
        static void Main(string[] args)
        {
            using (var rm = new RijndaelManaged())
            {
                byte[] encrypted = EncryptRijndaelManaged("Ala ma kota", rm.Key, rm.IV);
                string decrypted = DecryptRijndaelManaged(encrypted, rm.Key, rm.IV);
                Console.WriteLine(decrypted);
            }
        }

        public static byte[] EncryptRijndaelManaged(string input, byte[] key, byte[] IV)
        {
            byte[] encrypted;
            using (var rm = new RijndaelManaged())
            {
                rm.Key = key;
                rm.IV = IV;
                ICryptoTransform encryptor = rm.CreateEncryptor(rm.Key, rm.IV);
                using (var ms = new MemoryStream())
                {
                    using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        using (var swEncrypt = new StreamWriter(cs))
                        {
                            swEncrypt.Write(input);
                        }
                        encrypted = ms.ToArray();
                    }
                }
            }

            return encrypted;
        }

        public static string DecryptRijndaelManaged(byte[] input, byte[] key, byte[] IV)
        {
            string decrypted;
            using (var rm = new RijndaelManaged())
            {
                rm.Key = key;
                rm.IV = IV;
                ICryptoTransform decryptor = rm.CreateDecryptor(rm.Key, rm.IV);
                using (var ms = new MemoryStream(input))
                {
                    using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        using (var swDecrypt = new StreamReader(cs))
                        {
                            decrypted = swDecrypt.ReadToEnd();
                        }
                    }
                }
            }

            return decrypted;
        }
    }



2. Szyfrowanie asymetryczne


Jest to dużo bardziej bezpieczne rozwiązanie. Tworzone są dwa klucze:

  • prywatny - trzymany na serwerze w bezpiecznym i niedostępnym miejscy
  • publiczny - wysyłany do zainteresowanych
Szyfrowanie odbywa się za pomocą jednego z tych kluczy, a odszyfrowanie za pomocą drugiego. Jednym z najpopularniejszych algorytmów asymetrycznych jest RSA. Przykład szyfrowania i deszyfrowania:

Code:
        public static byte[] EncryptRsa(string input)
        {
            const int rsa = 1;
            var cspParameters = new CspParameters(rsa);
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);
            byte[] encrypted = rsaCryptoServiceProvider.Encrypt(new UnicodeEncoding().GetBytes(input), true);

            return encrypted;
        }

        public static string DecryptRsa(byte[] input)
        {
            const int rsa = 1;
            var cspParameters = new CspParameters(rsa);
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);
            var decrypted = rsaCryptoServiceProvider.Decrypt(input, true);

            return new UnicodeEncoding().GetString(decrypted);
        }



3. Hashowanie
Hashowanie jest procesem jednokierunkowym, to znaczy hashujemy podany ciąg znaków zmieniając je w kod, jednak w drugą stronę nie jesteśmy już w stanie (w prosty sposób) odczytać stworzonego hasha.
Istnieje jednak ryzyko, że hashując np. hasło w bazie pojawią się dwa podobne wpisy. Rozwiązaniem tego problemu jest dodanie tzw. soli (Salt) którą dodaje się np. do hasła i taki ciąg znaków dopiero zostaje hashowany. Dzięki takiemu rozwiązaniu mamy znikome prawdopodobieństwo powtórzenia się hashy.
Obecnie zalecane jest stosowane algorytmu SHA256. Przykład użycia algorytmu:

Code:
        public static string Sha256Encryption(string input, string salt)
        {
            byte[] data = new UnicodeEncoding().GetBytes(input + salt);
            var sha256Managed = new SHA256Managed();
            var computeHash = sha256Managed.ComputeHash(data);

            return ToHex(computeHash);
        }

        private static string ToHex(byte[] computeHash)
        {
            var hex = new StringBuilder();
            foreach (var b in computeHash)
            {
                hex.AppendFormat("{0:x2}", b);
            }

            return hex.ToString();
        }



4. Cyfrowy podpis
Cyfrowy podpis gwarantuje, że wiadomość została niezmieniona i wysłana przez daną instytucję.
Przykład implementacji:

Code:
        public static byte[] CreateSignature(string input)
        {
            const int rsa = 24;
            var cspParameters = new CspParameters(24);
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);

            return rsaCryptoServiceProvider.SignData(new UnicodeEncoding().GetBytes(input), "SHA256");
        }

        public static bool CheckSign(string input, byte[] signature)
        {
            const int rsa = 24;
            var cspParameters = new CspParameters();
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);

            return rsaCryptoServiceProvider.VerifyData(new UnicodeEncoding().GetBytes(input), "SHA256", signature);
        }


LINQ - użycie GroupJoin


Zadanie jest następujące:
Na podstawie poniższego schematu (baza AdventureWorksLT) wyznaczyć wartość zamówienia. Dodać kolumny z id klienta i zamówienia (bez uwzględnienia podatku, obniżek itp).





W zapytaniu wykorzystamy GroupJoin.

Zapytanie to zapiszemy w dwóch postaciach LINQ2SQ: metodowej oraz SQLo podobnej:

Code:
            var res = context.SalesOrderHeaders.
                              GroupJoin(context.SalesOrderDetails, header => header.SalesOrderID,
                                        detail => detail.SalesOrderID,
                                        (header, details) => new
                                                                 {
                                                                     CustomerId = header.CustomerID,
                                                                     OrderId = header.SalesOrderID,
                                                                     TotalPrice = details.Sum(x => x.UnitPrice * x.OrderQty)
                                                                 });


  Code:
             var res2 = from a in context.SalesOrderDetails
                       group a by a.SalesOrderID
                       into g
                       join b in context.SalesOrderHeaders on g.Key equals b.SalesOrderID
                       select new
                                  {
                                      CustomerId = b.CustomerID,
                                      OrderId = b.SalesOrderID,
                                      TotalPrice = g.Sum(x => x.UnitPrice*x.OrderQty)
                                  };





Obydwa zapytania zwracają te same rezultaty i są poprawne pod względem składni LINQ. Ciekawszym aspektem może być zapytanie SQL generowane przez te dwa zapytania. Dla pierwszego zapytania zostanie wygenerowany następujący kod SQL:
SELECT [Extent1].[SalesOrderID] AS [SalesOrderID], [Extent1].[CustomerID] AS [CustomerID],
(
SELECT
  
SUM([Filter1].[A1]) AS [A1]
  
FROM ( SELECT
      
[Extent2].[UnitPrice] *  CAST( [Extent2].[OrderQty] AS DECIMAL(19,0)) AS [A1]
      
FROM [SalesLT].[SalesOrderDetail] AS [Extent2]
      
WHERE [Extent1].[SalesOrderID] = [Extent2].[SalesOrderID]
  
)  AS [Filter1]) AS [C1]FROM [SalesLT].[SalesOrderHeader] AS [Extent1]



Drugie z zapytań wygenerowało następującego SQLa:


SELECT [Distinct1].[SalesOrderID] AS [SalesOrderID], [Extent2].[CustomerID] AS [CustomerID], [Extent2].[SalesOrderID] AS [SalesOrderID1],
(
SELECT
  
SUM([Filter1].[A1]) AS [A1]
  
FROM ( SELECT
      
[Extent3].[UnitPrice] *  CAST( [Extent3].[OrderQty] AS DECIMAL(19,0)) AS [A1]
      
FROM [SalesLT].[SalesOrderDetail] AS [Extent3]
      
WHERE [Distinct1].[SalesOrderID] = [Extent3].[SalesOrderID]
  
)  AS [Filter1]) AS [C1]FROM   (SELECT DISTINCT
  
[Extent1].[SalesOrderID] AS [SalesOrderID]
  
FROM [SalesLT].[SalesOrderDetail] AS [Extent1] ) AS [Distinct1]INNER JOIN [SalesLT].[SalesOrderHeader] AS [Extent2] ON [Distinct1].[SalesOrderID] = [Extent2].[SalesOrderID]


Rozwiązania podobne w składni a wynik zapytania SQL różny dla każdego z dwóch rozwiązań. Jeżeli weźmiemy pod uwagę wydajność to w przypadku pierwszego zapytania będzie ona większa. Jeżeli przyszłoby mi napisać samemu to zapytanie skorzystałbym z następującego zapytania:

SELECT header.SalesOrderID, header.CustomerID, details.TotalPriceFROM SalesLT.SalesOrderHeader header JOIN
  
(SELECT SUM(details.UnitPrice * details.OrderQty) AS TotalPrice, details.SalesOrderID
  
FROM SalesLT.SalesOrderDetail details
  
GROUP BY details.SalesOrderID) AS details ON header.SalesOrderID = details.SalesOrderID