Implementacja rozszerzeń systemu Dynamics 365 CE – Wstrzykiwanie zależności

W dzisiejszym odcinku cyklu „Wzorce projektowe w programowaniu systemu Dynamics 365 CE” przyjrzę się możliwości wykorzystania wstrzykiwania zależności w kodzie tworzonych przez nas rozszerzeń. Opiszę również przykładową implementację klasy pluginu, która wykorzystywać będzie wyżej wymieniony mechanizm za pomocą biblioteki Simple Injector (https://simpleinjector.org).

Czym jest owo legendarne wstrzykiwanie zależności? Tłumacząc w najprostszy sposób – polega ono na usunięciu bezpośrednich powiązań między wykorzystywanymi przez aplikację komponentami. Operacje tworzenia oraz łączenia obiektów są w tym przypadku często przeniesione z obiektów do dedykowanych klas (np. typu fabryka obiektów), a same obiekty „wstrzykiwane” są do konstruktorów za pomocą specjalistycznych kontenerów. Omawiany wzorzec pozwala nam na osiągnięcie luźnych powiązań między obiektami. Jest on również najbardziej popularnym sposobem realizacji paradygmatu odwrócenia sterowania (ang. „inversion of control”).

Wikipediową definicję wstrzykiwania zależności znajdziecie tutaj. Natomiast przykładową implementację wzorca w języku C# – w tym miejscu. Zainteresowanych zgłębianiem wiedzy na jego temat oraz stojących za nim paradygmatów odsyłam do niezmierzonych czeluści Internetu. Najpopularniejsza na świecie wyszukiwarka indeksuje pod hasłem „Dependency Injection” dziesiątki ciekawych artykułów oraz przykładów implementacji.  

Jak to wszystko ma się do implementacji rozszerzeń systemu Dynamics 365 CE? Przyjrzyjmy się typowej, stosowanej powszechnie trójwarstwowej architekturze kodu, która składa się z warstwy dostępu do danych, logiki biznesowej oraz warstwy, która jest uruchamiana bezpośrednio przez komponenty platformy (execution pipeline lub workflow foundation).  

Odpowiedzialności poszczególnych warstw przedstawiają się w powyższym przypadku następująco:

Entities – komponent współdzielony między wszystkimi warstwami. Zawiera definicję modelu klas, które reprezentują encję systemu, ich rozszerzenie oraz wszelkie pomocnicze elementy.

Data Access Layer – warstwa dostępu do danych. W przypadku omawianej aplikacji zawierać ona będzie repozytoria, odpowiedzialne za izolację logiki biznesowej od obiektów reprezentujących fizyczne dane w systemie (context, OrganizationService itp.).

Business Layer – warstwa logiki biznesowej. Kontener na wszelkiego rodzaju serwisy domenowe oraz komponenty odpowiedzialne za reprezentację procesów biznesowych.

Execution Layer – warstwa zawierająca klasy implementujące interfejsy IPlugin lub dziedziczące po klasie CodeActivity.

W omawianym przykładzie wykorzystamy opisywaną we wcześniejszych rozdziałach klasę PluginBase. Pierwszą rzeczą, którą musimy zrobić, będzie dodanie referencji do wybranej biblioteki DI. Istnieje w tym przypadku wiele dostępnych rozwiązań. Rozwiązania, z którymi najczęściej spotykałem się w czasie projektów, to: Windsor Castle, Ninject oraz Simple Injector. W dalszej części artykułu zaprezentuje użycie kontenera DI, korzystając z ostatniej z wymienionych powyżej bibliotek.

Po dodaniu do naszego komponentu referencji do wybranego kontenera DI – pierwszą rzeczą, którą będziemy musieli zrobić, będzie dodanie nowej metody abstrakcyjnej do klasy PluginBase:

public abstract void RegisterDependencies(Container container);

Następnie, w metodzie Execute klasy PluginBase dodamy następujący fragment kodu:

container.Register<ITracingService>(() => tracingService);
container.Register<IPluginExecutionContext>(() => pluginExecutionContext);
container.Register<IOrganizationServiceFactory>(() => serviceFactory);
container.Register<IRepositoryFactory>(() => new RepositoryFactory(container));
container.Register<IServicesFactory>(() => new ServicesFactory(container));

Powyższy kod odpowiada za rejestrację funkcji, które zostaną uruchomione w momencie próby skorzystania z konkretnego interfejsu. W przypadku pierwszych 3 linii, w momencie odwołania się do interfejsów zostaną zwrócone utworzone wcześniej obiekty, pochodzące z SDK systemu Dynamics 365. Z kolei w przypadku interfejsów IRepositoryFactory oraz IServicesFactory – za pomocą zarejestrowanej metody każdorazowo zostanie utworzony nowy obiekt implementujący ww. interfejsy.

Przykładową implementację klas RepositoryFactory oraz ServicesFactory znajdziecie poniżej:

public class ServicesFactory : IServicesFactory
{
        private Container container;

        public ServicesFactory(Container container)
        {
            this.container = container;
        }
        public T Get<T>() where T : IService
        {
            return (T)this.container.GetInstance(typeof(T));
        }
}
public class RepositoryFactory : IRepositoryFactory
{
        private Container container; 

        public RepositoryFactory(Container container)
        {
            this.container = container;
        }
        public T Get<E, T>() where E : Entity where T : IRepository<E>
        {
            var context = container.GetInstance<IPluginExecutionContext>();
            var orgServiceFactory = container.GetInstance<IOrganizationServiceFactory>();
            var instance = (T)container.GetInstance(typeof(T));
            instance.Initialize(orgServiceFactory, context.UserId);
            return instance; 
        }

        public T Get<E, T>(Guid userId) where E : Entity where T : IRepository<E>
        {
            var context = container.GetInstance<IPluginExecutionContext>();
            var orgServiceFactory = container.GetInstance<IOrganizationServiceFactory>();
            var instance = (T)container.GetInstance(typeof(T));
            instance.Initialize(orgServiceFactory, userId);
            return instance;
        }
}

Przyjrzyjmy się teraz w jaki sposób nasza klasa, wykorzystująca interfejs IPlugin będzie mogła skorzystać z ww. klas. Wyobraźmy sobie, że tworzone rozszerzenie o nazwie FooPlugin 😊 będzie wykorzystywał do pracy następujące komponenty:

OpportunityService – klasa zawierająca implementację logiki biznesowej

OpportunityRepository – klasa odpowiadająca za izolację kodu izolującego komponenty reprezentujące fizyczne dane od logiki biznesowej.

W „klasycznym” podejściu – obiekty obu wyżej wymienionych klas zostałyby utworzone za pomocą operatora new. Przy tym podejściu pojawiłaby się „silna” zależność, w którym obiekt z wyższej warstwy nie mógłby poprawnie funkcjonować bez znajomości konkretnej implementacji klasy obiektu z warstwy niższej. Jest to niewłaściwa praktyka, prowadząca do tworzenia kodu, w którym występują wielowarstwowe zależności i który może być przez to niezwykle trudny do przetestowania za pomocą testów automatycznych.  

W przypadku wykorzystania kontenera Dependency Injection wykorzystywać będziemy zarejestrowane wcześniej klasy. Obiekty tych klas będą tworzone automatycznie w czasie wykonywania kodu. Następnie będą „wstrzykiwane” do konstruktorów klas z warstw, które w klasycznym podejściu byłyby od nich zależne.

Na początku przeciążmy w kodzie klasy FooPlugin zdefiniowaną w klasie bazowej metodę abstrakcyjną RegisterDependencies:

public override void RegisterDependencies(Container container)
{         
	container.Register<IOpportunityRepository, OpportunityRepository>();
	container.Register<IOpportunityService, OpportunityService>();
}

Powyższy kod dokonuje rejestracji typów, których instancje będą zwracane w momencie, w którym aplikacja będzie musiała z nich skorzystać.

UWAGA: Różne kontenery DI stosują różne domyślne podejścia do tworzenia zarejestrowanych obiektów. Określa to tzw. styl życia obiektu (LifeStyle). Wykorzystywana biblioteka Simple Injector domyślnie wykorzystuje przemijający (transient) styl życia obiektu. W momencie próby pobrania z kontenera obiektu dla zarejestrowanego typu – domyślnie każdorazowo zostanie utworzona jego nowa instancja. Oczywiście zachowanie to możemy zmienić, jawnie podając w momencie rejestracji typu sposób tworzenia obiektu (inne dostępne to: Singleton, w którym każdorazowo zwracany będzie ten sam obiekt przechowywany w pamięci, oraz Scoped – w którym istniejąca instancja obiektu jest reużywana w ramach zdefiniowanego bloku kodu). Inne biblioteki DI mogą mieć zdefiniowany inny domyślny styl życia obiektów dla rejestrowanych typów. Przykładowo – w popularnej bibliotece Windsor Castle domyślnym stylem życia obiektów jest Singleton.

Przejdźmy teraz do implementacji metody Execute naszej klasy FooPlugin.

public override void Execute(IPluginExecutionContext pluginExecutionContext, Container container)
{
	var target = this.GetTargetEntity<Contact>(pluginExecutionContext);
	var factory = container.GetInstance<IServicesFactory>(); 

	var testService = factory.Get<IOpportunityService>(); 
	testService.DoSomething(target); 
}

Widzimy, że w porównaniu z prezentowanymi we wcześniejszych rozdziałach przykładami, nastąpiła zmiana argumentów przekazywanych do metody Execute. Aktualnie drugim z argumentów jest utworzona w klasie bazowej PluginBase instancja klasy reprezentującej kontener DI. Za jego pomocą pobieramy fabrykę obiektów implementujących interfejs IService. Następnie za pomocą fabryki tworzymy instancję obiektu typu IOpportunityService, którego metodę DoSomething uruchamiamy w ostatniej linii omawianej metody.

Na powyższym przykładzie widzimy, że w porównaniu z klasycznym podejściem, w którym instancja klasy typu IPlugin tworzyłaby nowe obiekty typu IService za pomocą operatora new, nastąpiło odwrócenie zależności. W naszym przypadku klasa FooPlugin sama decyduje jakie konkretne typy, implementujące dany interfejs będzie wykorzystywać (rejestracja w przeciążonej metodzie RegisterDependecies). Natomiast kontener DI odpowiada za utworzenie oraz wstrzyknięcie instancji obiektów tych klas na etapie uruchamiania kodu.

Na końcu przyjrzyjmy się, w jaki sposób możemy użyć opisanej powyżej techniki do implementacji testu jednostkowego. Poniższy przykład ilustruje, w jaki sposób zastąpić instancję klasy OpportunityService klasą pozorowaną (Mock), implementującą interfejs IOpportunityService i utworzoną za pomocą frameworka Moq.

var container = new Container();
container.Register<IServicesFactory, ServicesFactory>();

var inputParameters = new ParameterCollection();
inputParameters.Add("Target", new Contact());

var pluginExecutionContextMock = new Mock<IPluginExecutionContext>();
pluginExecutionContextMock.Setup(m => m.InputParameters).Returns(inputParameters);
var opportunityTestServiceMock = new Mock<IOpportunityService>();
opportunityTestServiceMock.Setup(m => m.DoSomething(It.IsAny<Contact>())).Callback(() => 
{
	Console.WriteLine("Method executed");
}); 

var fooPlugin = new Plugins.FooPlugin(String.Empty, String.Empty);
fooPlugin.Execute(pluginExecutionContextMock.Object, container);

Pełen kod źródłowy opisywanej aplikacji znajdziecie pod adresem: https://github.com/gashupl/dyn365devbestpractices/tree/master/XrmLabs.Blog.Dyn365BestPractices/Chapter04/Chapter04.Plugins

Total Views: 478 ,
This Article Has 1 Comment
  1. Pingback: dotnetomaniak.pl

Dodaj komentarz

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