Zastosowanie wzorca repozytorium w programowaniu rozszerzeń systemu Dynamics 365 CE

Wprowadzenie

W dzisiejszym odcinku przyjrzymy się zastosowaniu wzorca repozytorium do organizacji dostępu do danych w rozszerzeniach .NET systemu Dynamics 365 CE (pluginy oraz niestandardowe aktywności workflow). W najprostszym przypadku operacje na danych w omawianym systemie możemy wykonywać, korzystając z interfejsu IOrganizationService. Dostęp do stosownego obiektu, który implementuje wspomniany interfejs, zapewnia nam natomiast OrganizationServiceFactory. IOrganizationService daje nam możliwość wykonywania operacji typu CRUD (Create, Retrieve, Update, Delete) na danych oraz kilku innych operacji specyficznych dla systemu Dynamics 365CE (szczegółowe informacje znajdziecie w tym miejscu). Kolejny poziom abstrakcji wprowadza LINQ 2 CRM provider wraz z powiązanymi narzędziami (ServiceContext, klasy Proxy). Wspomniane mechanizmy umożliwiają operowanie na rekordach w systemie Dynamics 365 CE z poziomu kodu w podobny sposób, w jaki robimy to, wykorzystując klasyczne biblioteki ORM.

Repozytorium w rozszerzeniach systemu Dynamics 365

W tym miejscu do głowy może przyjść Wam pytanie: do czego tak naprawdę przydaje się wzorzec repozytorium? Czy z racji dostępności wygodnych technik oraz narzędzi programistycznych wprowadzania kolejnej warstwy abstrakcji jest rzeczywiście przydatne? W moim przekonaniu odpowiedź brzmi: jak najbardziej :). Zastosowanie wspomnianego wzorca umożliwia separację logiki biznesowej od fizycznego dostępu do źródeł danych (usługi sieciowe, bazy danych itp.), wymusza swoiste uporządkowanie kodu odpowiedzialnego za operacje na rekordach w systemie, a przede wszystkim, w powiązaniu z innymi technikami (fabryki, wstrzykiwanie zależności) daje możliwość pisania kodu, który jest niezwykle prosty do automatycznego testowania.

W dalszej części artykułu przyjrzymy się przykładowej implementacji wzorca repozytorium, którą będziemy mogli wykorzystać w naszych rozszerzeniach.

Przykładowa implementacja

W najprostszej wersji interfejs opisujący przykładowe repozytorium, które udostępnia informacje o dostępnych w systemie szansach sprzedaży, może wyglądać w następujący sposób:

Powyższy interfejs opisuje repozytorium udostępniające programistom operacje pobrania rekordu na podstawie znanego identyfikatora oraz utworzenia nowego rekordu szansy sprzedaży w systemie (w celu uproszczenia kodu nie zawiera on innych metod takich jak pobranie kolekcji rekordów na podstawie zadanego zapytania, aktualizacja lub usunięcie rekordu z bazy danych).

Implementacja wspomnianego interfejsu może wyglądać następująco:

Powyższy kod wykorzystuje obiekt OrganizationServiceContext w celu uzyskiwania dostępu do danych. Obiekt ten umożliwia wysyłanie zapytań do systemu, wykorzystując do tego składnie LINQ. Dodatkowo umożliwia operowanie rekordach w kontekście lokalnie oraz zapisanie wszystkich zmian w bazie za pomocą jednorazowego wywołania metody SaveChanges (uwaga, metoda ta nie zapewnia transakcyjności wykonywanych operacji na danych).

Inne podejście do implementacji repozytorium polega na bezpośrednim uruchamianiu metod obiektu OrganizationService. Różnica polega w tym przypadku na tym, że każdorazowe uruchomienie metody repozytorium odwołuje się oraz wywołuje operacje bezpośrednio na serwisie SOAP systemu Dynamics 365. Przykładowy kod repozytorium, które korzysta z obiektu OrganizationService:

W praktyce najczęściej stosuje się podejście mieszane i korzysta z OrganizationService’u lub z kontekstu w miarę potrzeb. 

Wszystko wygląda dobrze. Zauważmy jednak, że kolejne, powiązane z innymi encjami i dodawane do naszego rozwiązania repozytoria będą często zawierać podobny lub nawet identyczny zestaw metod do wykonywania operacji CRUD. Może to prowadzić do pojawiania się w systemie zduplikowanego kodu odpowiedzialnego za wykonywanie identycznych operacji w wielu miejscach. Uniknąć tej sytuacji możemy, tworząc bazowe generyczne repozytorium, po którym będą dziedziczyć inne klasy. Przykładowy interfejs opisujący ww. klasę (ponownie zaprezentuję jego implementację jedynie dla metod GetById oraz Create, nie dla pełnego „kruda”) zamieszczam poniżej.

Implementacja klasy bazowej wygląda natomiast następująco:

Dzięki zastosowaniu powyższej klasy bazowej, implementując repozytorium powiązane z konkretną encją, możemy pominąć podstawowe operacje takie CRUD i skoncentrować się na implementacji metod, specyficznych dla danego typu rekordu.

Poniższy przykład prezentuje przykładowy interfejs IOpportunityRepository oraz implementującą go klasę, która wykorzystuje opisane powyżej komponenty bazowe.

To na razie wszystko na temat wzorca repozytorium. W kolejnych odcinkach przyjrzymy się, w jaki sposób wykorzystać go w połączeniu ze wzorcami fabryki (factory) oraz wstrzykiwania zależności (dependency injection) w celu uzyskania uporządkowanego i testowalnego kodu.

Źródła, które zostały zaprezentowane w tym artykule, znajdziecie pod adresem:

https://github.com/gashupl/dyn365devbestpractices/tree/master/XrmLabs.Blog.Dyn365BestPractices/Chapter%2002/Chapter02.Repositories

Total Views: 1408 ,
This Article Has 3 Comments
  1. Pingback: dotnetomaniak.pl

  2. Łukasz Reply

    Jednym z problemów takiego rozwiązania będzie wydajność. Pobieranie po ID robi „SELECT *” więc będzie wielokrotnie wolniejsze od pobrania tylko koniecznych pól. Brakuje metod do pobranie więcej niż jednego rekordu (GetAll)? Widziałem rozwiązania z opcjonalnym parametrem typu string[] attributes, ale to strasznie brzydkie i niewygodne w użyciu.

    Co z filtrowaniem? Samo bazowe repozytorium jest w zasadzie bezużyteczne. Metody są skrajnie płytkie. W czym „public void SaveChanges()” jest lepsze od this.ServiceContext.SaveChanges();? Pozostałe są równie banalne.

    Kolejny problem pojawia się przy rozwijaniu jakiejkolwiek logiki biznesowej. Potrzebujemy pobrać jakieś dane przechodząc przez kilka powiązanych ze sobą encji. Sugerujesz utworzenie z tej okazji dedykowanej metody – OK, tyle że w dużym projekcie takich metod będzie zaraz setki. Reusability tego jest słabe ponieważ każdy developer będzie miał jakieś szczególne przypadki, z kolei zmiana cudzego kodu może generować błędy. Jest to generalny problem dla którego repozytorium uchodzi raczej za antywzorzec (jest na ten temat mnóstwo materiałów, więc nie będę powielał argumentów).

    Swego czasu używałem tego podejścia w paru projektach, głównie z tego względu że robiąc interfejs wokół takiego repozytorium można je było stosunkowo łatwo „zmockować” w testach jednostkowych. Mając jednak takie narzędzia jak FakeXrmEasy jest to zupełnie niepotrzebne.

    Obecnie użycie tego wzorca bym odradzał. Sam „XrmContext” jest już wystarczającą abstrakcją, a kod w stylu:
    var account = crm.AccountSet.FirstOrDefault(a => a.Name.Where(„x”) && a.ParentCustomerId == null).Select(a => a.Id).FirstOrDefault() całkowicie czytelny i bardzo ekspresyjny. Nie ma sensu tworzenie repozytorium i metody w stylu „Guid FindAccountWhereNameStartsWithAndParentCustomerIsNull(string nameStartsWith)”.

    Kolejna wada to, że takie repozytorium z czasem staje się śmietnikiem. Mamy nasze AccountRepository i walimy tam cały kod który ma cokolwiek wspólnego z klientami. Odniesienia do tego kodu są z kilkunastu modułów, a każda zmiana powoduje rekompilację 3/4 solucji. Niezbyt to czyste.

    Ciekaw jestem co na ten temat sądzisz. Blog strasznie fajny. Chyba żadnego innego w tym stylu po polsku nie ma.

    • PG Reply

      Ok. Postaram się ustosunkować do kolejnych poruszonych tematów:

      1. Pobieranie danych oraz filtracja.
      Zgadzam się, że prezentowane klasy nie umożliwiają określenia listy zwracanych atrybutów oraz filtracji pobieranych danych. Zdecydowałem się jednak na taki zabieg celowo (o czym wspominam w artykule) w celu maksymalnego uproszczenia prezentowanego kodu. Moim celem nie była implementacja „pełnego” repozytorium posiadającego komplet metod (w rzeczywistym projekcie w repozytorium bazowym znajdzie się prawdopodobnie kilkanaście różnych metod), a jedynie zaprezentowanie sposobu podejścia do implementacji omawianego wzorca w kontekście systemu Dynamics 365 CE.

      2. Zapytania „przechodzące” przez kilka powiązanych encji.
      W przypadku zapytań zwracających kolekcję różnych typów danych lub też zbiory będące wynikiem „sklejenia” danych z wielu źródeł – preferuje używanie serwisów domenowych. Repozytorium traktuję w tym przypadku jako reprezentację fizycznego zbioru danych.

      3. Zmiany w „cudzym” kodzie – najlepszym sposobem walki z błędami powstałymi w wyniku modyfikacji kodu autorstwa kogoś innego – jest stosowanie testów jednostkowych 🙂

      4. XrmContext jako wystarczająca abstrakcja – w tym przypadku zupełnie się nie zgadzam. Definiowanie zapytań do fizycznego zbioru danych (nie ważne czy będzie to zapytanie LINQ, SQL, czy w innym języku) na poziomie logiki biznesowej w żadnym przypadku nie jest dobrym pomysłem. Oczywiście narzędzia w rodzaju FakeXrmEasy umożliwiają tworzenie testów w takich przypadkach, jednak nie zmienia to faktu, że stosowanie w tym przypadku dodatkowych abstrakcji (a taką abstrakcją jest właśnie obiekt reprezentujący repozytorium) jest pożyteczną rzeczą, eliminującą konieczność duplikacji kodu w wielu miejscach w systemie. Nie wyobrażam sobie sytuacji, w której kod pobierający z bazy danych wszystkich aktywnych klientów (gdzie w celu określenia tego, czy dana organizacja jest aktywnym klientem, wymagane jest kilkunasto-linijkowe zapytanie LINQ) miałby znajdować się w kilku miejscach w systemie. Nieco inaczej wynika kwestia w przypadku danych, które już znajdują się w pamięci aplikacji, ale w tym przypadku rozwiązaniem może być ponownie stosowanie serwisów domenowych.

      5. Śmietnik – przyznam, że również zupełnie nie trafia do mnie ten argument. To w jaki sposób utrzymujemy nasz kod, zależy jedynie od nas. Nie twierdzę również, że wszystkie repozytoria dla danego rozwiąznia powinniśmy przechowywać w ramach pojedynczej biblioteki. Oczywiście na początku jest to zazwyczaj najłatwiejsze podejście, ale w miarę rozwoju projektu powinniśmy dokonywać ciągłego refaktoringu oraz wymaganych zmian w architekturze aplikacji.

      Dziękuję za komentarz i pozdrawiam.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *