Jakiś czas temu (kilka miesięcy;)) obiecywałem, ze napiszę artykuł o WCF Data Service. Zainteresowanych odsyłam tutaj.
Category Archives: EntityFramework
Entity Framework – aplikacja trójwarstwowa
Jakiś czas temu napisałem artykuł o podstawach EF. Jeśli ktoś interesuje się EF to możliwe, że zaciekawi go mój nowy artykuł o wykorzystaniu Entity Framework w aplikacji trójwarstwowej – warstwa prezentacji jest oddzielona usługą sieciową (WCF) od warstwy dostępu dodanych.
Miłego czytania: http://msdn.microsoft.com/pl-pl/library/ff714342.aspx
MaxLength w EntityFramework oraz Text Template Transformation Toolkit (T4).
Podczas generowania modelu encji na podstawie bazy danych, EF potrafi pobrać maksymalną długość pola. Jeśli kolumna w bazie posiada typ nvarchar(50) to EF zmapuje to na zmienną typ string oraz ustawi pole MaxLength na wartość 50.
Informacja o dozwolonej długości (50) jest zapisana w metadanych. Niestety próba przypisania wartości dłuższej niż 50 znaków zakończy się powodzeniem ponieważ nvarchar(50) został zmapowany na string, który nie ma ograniczenia do 50 znaków. Poniższy kod NIESTETY zadziała:
Contact contact = new Contact(); contact.Email="jakis napis dluzszy niz 50 znakow..."
W jaki sposób walidować więc dane? Gdy przekażemy napis dłuższy niż 50 znaków wygenerowana encja nie wyrzuci wyjątku jednak podczas zapisu informacji danych do bazy danych oczywiście wyjątek zostanie zwrócony:
String or binary data would be truncated.
Należy zatem w jakiś sposób walidować dane podczas próby przypisania ich do konkretnych właściwości. Jednym ze sposobów jest wykorzystanie klas częściowych. Można w końcu stworzyć klasę, która będzie walidowała dane np:
public partial class Contact { public IEnumerable<RuleViolation> GetRuleViolations() { if (Email.Length>50) yield return new RuleViolation("Nieprawidłowa długość pola email.", "Email"); } public void Validate() { if (GetRuleViolations().Count() != 0) throw new Validation.ValidationException("Encja zawiera nieprawdiłowe dane"); } }
gdzie RuleViolation to:
public class RuleViolation { public string ErrorMessage { get; private set; } public string PropertyName { get; private set; } public RuleViolation(string errorMessage, string propertyName) { ErrorMessage = errorMessage; PropertyName = propertyName; } }
Rozwiązanie ma zasadniczą wadę – każde pole trzeba ręcznie sprawdzać. Jak już wspomniałem EF posiada w metadanych MaxLength więc pomyślny jak można wykorzystać już zapisane dane.
Rozwiązaniem jest tzw. Text Template Transformation Toolkit (T4). T4 jest silnikiem automatycznego generowania kodu. Za pomocą szablonów T4 możemy generować klasy, metody itp. Szablony tworzone są za pomocą specjalnego języka dyrektyw przypominającego nieco dyrektywy ASP .NET. Wykonanie T4 składa się z następujących etapów:
Po skompilowaniu szablonu zostanie stworzona klasa GeneratedTextTransformation (wraz z przeładowaną metodą TransformText) odpowiedzialna za wygenerowanie kodu opisanego przez Text Template. Nie będę w tym poście opisywał dokładnie składni T4 ponieważ jest to zdecydowanie temat wymagający przynajmniej kilku postów. Przedstawię tylko przykładowy szablon:
<#@ template debug="false" hostspecific="false" language="C#" #> <#@ output extension=".cs" #> <#Write("class HelloClass\n{");#> public void HelloWorld() { } }
Kod wygenerowany na podstawie powyższego szablonu:
class HelloClass { public void HelloWorld() { } }
Wróćmy do głównego tematu. Klasy encji EF generowane są właśnie na podstawie T4. Aby zobaczyć kod T4 wystarczy kliknąć prawym guzikiem myszki na modelu encji (np. plik Entities.edmx) i wybrać Add Code Generation Item. Pojawi się okno w którym zaznaczamy ADO .NET EntityObject Generator.
Powstanie plik o rozszerzeniu .tt. Nie będę tutaj wklejał jego zawartości ponieważ jest zdecydowanie zbyt długa :). Szablon generuje klasy wszystkich encji. Jeśli szablon został nazwany Model1.tt to Model1.cs będzie zawierał wygenerowane klasy encji w C#.
Modyfikując szablon możemy więc wpływać na zachowanie generowanych encji. Nie ma więc problemu abyśmy dopisali logikę walidacyjną odpowiedzialną za sprawdzanie długości string’a. W tej chwili szablon generuje właściwości w następujący sposób:
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)] [DataMemberAttribute()] public global::System.String Email { get { return _Email; } set { OnEmailChanging(value); ReportPropertyChanging("Email"); _Email = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("Email"); OnEmailChanged(); } }
Z kolei my chcemy aby przypisanie właściwości wyglądało:
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)] [DataMemberAttribute()] public global::System.String Email { get { return _Email; } set { if (value!=null&& value.Length > 50) { throw new PoliticiansPromises.Entities.Validation.ValidationPropertyException(String.Format("Pole {0} może mieć maksymalnie {1} znaków.",PoliticiansPromises.Dictionaries.PropertyMappingsManager.Instance["Email"], "50"),Email); } OnEmailChanging(value); ReportPropertyChanging("Email"); _Email = StructuralObject.SetValidValue(value, true); ReportPropertyChanged("Email"); OnEmailChanged(); } }
Można to zrealizować w łatwy sposób dopisując odpowiedni kod do szablonu T4. Największą chyba trudnością jest znalezienie w tym długim szablonie fragmentu odpowiedzialnego za generowanie setter’a. W każdym razie fragment T4 odpowiedzialny za wygenerowanie logiki walidacyjnej wygląda następująco:
<#+ if (code.Escape(primitiveProperty.TypeUsage) == "global::System.String") { string facetName = "MaxLength"; int maxLength = 0; if (Int32.TryParse(primitiveProperty.TypeUsage.Facets[facetName].Value.ToString(), out maxLength)) { #> if (value!=null&& value.Length > <#= maxLength.ToString() #>) { throw new PoliticiansPromises.Entities.Validation.ValidationPropertyException(String.Format("Pole {0} może mieć maksymalnie {1} znaków.","<#= code.Escape(primitiveProperty) #>", "<#= maxLength.ToString() #>"),<#=code.Escape(primitiveProperty)#>); } <#+ } } #>
Najpierw sprawdzamy czy właściwość jest typu String a następnie pobieramy z metadanych maksymalną długość. Po dopisaniu powyższego fragmentu w odpowiednim miejscu wszystkie encje zostaną wygenerowane ze stosowną logika walidacyjną. Kod powinien być dopisany bezpośrednio po następującym fragmencie T4:
<#=code.SpaceAfter(Accessibility.ForSetter((primitiveProperty)))#>set { <#+ if (ef.IsKey(primitiveProperty)) { if (ef.ClrType(primitiveProperty.TypeUsage) == typeof(byte[])) { #> if (!StructuralObject.BinaryEquals(<#=code.FieldName(primitiveProperty)#>, value)) <#+ } else { #> if (<#=code.FieldName(primitiveProperty)#> != value) <#+ } #> { <#+ PushIndent(CodeRegion.GetIndent(1)); } #>
Od teraz programista po dodaniu nowej encji nie musi pisać żadnego kodu odpowiedzialnego za walidacje długości samodzielnie.
Entity Framework – aplikacja dwuwarstwowa
Entity Framework stanowi doskonałą alternatywę dla uproszczonego LINQ to SQL. Wszystkich zainteresowanych poznaniem podstaw EF zachęcam do przeczytania mojego artykułu na MSDN:
WCF Data Services
WCF Data Service to usługa sieciowa umożliwiająca łatwy dostęp do danych. Wyobraźmy sobie następujący przypadek:
Mamy pewną bazę danych zawierającą np. informacje o produktach. Można napisać ręcznie usługę WCF, która wyeksponuje wszelkie potrzebne dane za pomocą metod. Usługa w takim przypadku zawierałaby metody typu Create, Update, Delete, GetById, GetByQuery itp. Implementacja usługi dla każdej tabeli w bazie jest dość czasochłonna i niezbyt interesująca. Za pomocą WCF Data Service, usługa zostanie stworzona automatycznie na podstawie modelu encji (np. Entity Framework). Ponadto komunikacja między klientem a WCF może odbywać się za pomocą zapytań LINQ, tak jakbyśmy korzystali z lokalnego Entity Framework.
WCF Data Service oparty jest o architekturę REST (tzw. RESTful Service). Architektura REST składa się z serwera oraz klienta. Klient wysyła żądanie do serwera, które następnie jest przetwarzane. Serwer ma dostęp do zasobów i zwraca tzw. reprezentację zasobów (representation of resource), która przeważnie przyjmuje postać dokumentu XML. Zasoby muszą mieć jakiś jednoznaczny identyfikator (np. URI). Serwer nie przechowuje informacji o stanie – to klient odpowiedzialny jest za przejścia między różnymi stanami.
Architektura REST musi spełniać następujące warunki:
• Bezstanowość ,
• Buforowanie danych,
• SoC (separation of concerns),
• Budowa warstwowa,
• Ujednolicony protokół komunikacji.
Sama idea architektury jest stara (po raz pierwszy została przestawiona w pracy doktorskiej Roya T. Fieldinga, 2000). W praktyce protokołem komunikacji między serwerem a klientem jest najczęściej HTTP. W takim przypadku identyfikatorem zasobów jest zwykły URL (http://www.localhost/users). W zależności od użytej metody HTTP, serwer wykona jedną z następujących operacji:
- HTTP POST – tworzenie zasobu,
- HTTP GET – selekcja danych ,
- HTTP PUT – aktualizacja,
- HTTP DELETE – usunięcie danych.
Przykładowo, poniższe zapytanie spowoduje pobranie listy użytkowników z bazy danych:
GET /users HTTP/1.1 Host: localhost Accept: application/xml
Usługa sieciowa, po otrzymaniu takiego zapytania wywoła metodę odpowiedzialną za selekcję danych z kolekcji encji users. Architektura WCF Data Service wygląda następująco (źródło MSDN):
Klient komunikuje się z usługą za pomocą HTTP. Następnie Data Services Runtime wykorzystując np. Entity Framework pobiera dane z bazy. Możliwe jest napisanie własnych provider’ów, które mogą nawet odwoływać się do baz nierelacyjnych.
Myślę, że teorii na początek wystarczy. Więcej informacji o REST znajdziecie np. na wiki :). Skupmy się teraz jak to wygląda od strony praktycznej. Większość operacji wykona za nas IDE:
- Na początek należy stworzyć oczywiście projekt WCF Service Application.
- Po utworzeniu nowego projektu, dojemy model encji ADO .NET Entity Data Model (Add -> New Item -> Data ->ADO .NET Entity Data Model). Kreator poprowadzi nas i np. wygeneruje model encji na podstawie bazy danych .
- Następnie dodajemy Data Service (Add -> New Item -> Web -> WCF Data Service).
- Zostanie wygenerowany szablon usługi. Należy teraz wstawić w miejsce komentarza prawidłowy model encji. Po zmianach kod powinien wyglądać np. następująco:
public class SalesSystemService : DataService<DataServicesEntities> { }
-
WCF Data Service pozwala zdefiniować prawa dostępu. Można umożliwić klientom np. dostęp tylko do odczytu do produktów:
public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead); }
W zasadzie więcej konfiguracji nie trzeba! Wystarczyło stworzyć model encji, DataService, podmienić typ generyczny oraz skonfigurować prawa dostępu. W tej chwili możemy odpalić usługę i za pomocą przeglądarki przetestować jej działanie.
Aby uzyskać listę wszystkich produktów wystarczy w przeglądarce wpisać http://localhost:5744/Services/SalesSystemService.svc/Products. Jeśli chcemy aby został zwrócony tylko pojedynczy produkt o wskazanym kluczu głównym wystarczy wpisać http://localhost:5744/Services/SalesSystemService.svc/Products(4). Do dyspozycji jest naprawdę wiele sposobów selekcji. Przykładowo http://localhost:5744/Services/SalesSystemService.svc/Products?$filter=BarCode eq ’55’ zwróci produkty tylko o kodzie kreskowym równym 55.
Wszystkie możliwe zapytania są tematem na osobny post. Microsoft dostarcza biblioteki ułatwiające komunikacje z WCF Data Service. Użytkownik (programista) nie musi ręcznie pisać adresów URL. Może skorzystać z API oraz języka LINQ. Jak to wygląda po stronie klienta przedstawię już w następnym poście.
Aktywny rekord i Entity Framework
Dzisiaj krótki post o wsparciu narzędzi ORM (konkretnie EF) da wzorca aktywny rekord. Tak naprawdę to co generuje nam EF jest już aktywnym rekordem. Wystarczy tylko uzupełnić wygenerowane klasy o logikę biznesową ponieważ w przeciwnym wypadku będzie to tylko czysta warstwa dostępu do danych.
Załóżmy, że mamy już wygenerowany jakiś diagram encji EF.Na tą chwile mamy wyłącznie zaimplementowaną (a raczej wygenerowaną) warstwę dostępu do danych. W celu dodawania właściwej logiki biznesowej, należy stworzyć klasy częściowe dla wygenerowanych encji. Przypuśćmy, że w EF zmalowaliśmy tabelę Orders na encję Order. Aby dodać logikę biznesową powinniśmy napisać:
public partial class Order { public bool Validate() { // ... } public int ComputeTotalDiscount() { // ... } public int EstimateDeliveryTime() { // ... } }
Jak widać użycie AR w połączeniu z ORM jest bardzo proste. Nie piszemy żadnego nadmiarowego kodu. Wszelki powtarzalny kod (DAL) wygeneruje za nas EF. W naprawdę dużych projektach takie rozwiązanie jednak ma kilka istotnych wad ale o nich napisze w następnych poście, który zostanie poświęcony Domain Model.
EntityFramework i wartość domyślna wyliczana na podstawie funkcji
Domyślne wartości bardzo łatwo ustawić za pomocą wizualnego edytora EntityFramework. Wystarczy ustawić właściwość Default w oknie properties:
Co jednak gdy chcemy ustawić wartość wyliczoną na podstawie jakieś funkcji? Dla przykładu może być to DateTime.Now bądź też Guid.NewGuid()?Wpisując w te same okienko dostaniemy błąd podczas kompilacji:
Error 1 Error 54: Default value (System.Guid.NewGuid()) is not valid for GUID. The value must be enclosed in single quotes in the form 'dddddddd-dddd-dddd-dddd-dddddddddddd'. Persons.edmx 446 11 Crm
Na szczęście rozwiązanie jest bardzo proste – wystarczy stworzyć klasę Partial i w konstruktorze ustawiać wartość domyślną:
public partial class Person { public Person() { this.ID_PERSON = Guid.NewGuid(); } }
Takim sposobem możemy manipulować danymi w dowolny sposób. Wszelkie konwersje danych nie są teraz problemem.