Plugin írása Dataverse-hez

Általában modell-vezérelt alkalmazásokban, mint pl. Dynamics 365 vagy más egyedi appban jellemző, hogy egyedi üzleti logikára van szükségünk. Ebben a bejegyzésben készítünk egy ún. plugin-t, amellyel a kapcsolattartó rekordra másoljuk a kapcsolattartóhoz rendelt partneren található telefonszámot. A plugin egy C#-ban megírt egyedi logika, amelyet a Dataverse-be feltölthetünk és különböző eseményekhez kötve automatikusan futtatásra kerülnek. Igyekszem a lehető legkevésbé „tekisen” magyarul elmagyarázni, ami az angol szaknyelv miatt nem egyszerű.

Műveletek végrehajtásának folyamata a Dataverse-ben

Ahhoz, hogy megértsük, hogy mikor és hogyan futnak le a plugin-ek meg kell említeni, hogy a Dataverse egy ún. „event framework” segítségével ad lehetőséget számunkra, hogy különböző eseményekhez köthessünk a plugin-ek futtatását. Ilyen események lehetnek, amikor például létrehozunk (Create), módosítunk (Update), törlünk (Delete), lekérünk (Retrieve) egy rekordot vagy hozzárendelünk egymáshoz rekordokat (Associate). Az események üzenetekként vannak továbbítva benne az adatinkkal és kerülnek feldolgozásra 4 fázisban az alábbiak szerint:

Event Exection Pipeline
Event Exection Pipeline (source)

Név Leírás
PreValidation Első fázis és lehetőség az adataink validációjára még mielőtt az adatbázis tranzakción belülre kerülünk. Így bármilye művelet nélkül megszíthatjuk a folyamatot és dobhatunk hibaüzenetet a felhasználó felé szinkron módban.
PreOperation Itt a mentés előtt műveleteket hajthatunk végre az adatainkon.
Mivel ez az adatbázis tranzakción belül zajlik, így hiba esetén minden változás visszaállításra kerül (rollback), ami erőforrás igényes, ezért az adatvalidációt igyekezzünk az előző fázisban végezni. Ezt szintén szinkron módon futtatjuk.
MainOperation Fő művelet, a módosításaink mentésre kerülnek.
PostOperation Mentés utáni műveleteket végezhetünk.
Fontos, hogy aszinkron plugin-t ilyen módban futtatunk.
Execution pipeline

Projekt létrehozása Visual Studio 2022-vel

Hozzunk létre egy „Class Library (.NET Framework)” típusú projektet:

Create new project in Visual Studio
Projekt létrehozása

Ügyeljünk rá, hogy a verzió .NET Framework 4.6.2 legyen:

Configure your new project
Projekt elnevezése és keretrendszer kiválasztása

A projekt létrehozását követően telepítsük a szükséges SDK-t a „Microsoft.CrmSdk.CoreAssembly” NuGet csomagot:

Install SDK from NuGet
SDK telepítése

Plugin logika megírása

Az előzőleg telelpített SDK tartalmazza az „Xrm.Sdk”-t, ami biztosítja az alap objektumokat és interfészeket, hogy kapcsolódhassnuk a Dataverse-hez és hogy műveleteket végezhessünk.

using Microsoft.Xrm.Sdk;

Amikor egy alap plugin-t hozunk létre, akkor minden esetben implementálnia kell az „IPlugin” interfészt, ami tartalmaz egy „Execute” függvényt, amin keresztül majd a plugin-ünk megkaphatja a kontextust és a különböző szolgáltatásokat. Ezen szolgáltatások közül nekünk talán a legfontosabb és leggyakrabban használt az „IOrganizationService”, amely segítségével kapcsolódhatunk és műveleket végezhetünk a Dataverse-ben. Továbbá fontos, hogy minden fájl, ami használja az IPlugin interfészt, az fel lesz ismerve, mint „plugin step”, tehát csak ezáltal tudjuk használni majd a Dataverse-ben.

Az „IPluginExecutionContext” tartalmaz minden a futtatás során használt adatot, köztük a módosításainkat is, amelyeket szeretnénk elmenteni. Legtöbb esetben a „InputParameters” tartalmazza a változásainkat egy „Target” property-ben:

var isTarget = executionContext.InputParameters.Contains("Target");
var isTargetTypeEntity = executionContext.InputParameters["Target"] is Entity;

if (!isTarget || !isTargetTypeEntity) return;

// Obtain the target entity from the input parameters.
var target = (Entity)executionContext.InputParameters["Target"];
var isTarget = executionContext.InputParameters.Contains("Target");
var isTargetTypeEntity = executionContext.InputParameters["Target"] is Entity;

if (!isTarget || !isTargetTypeEntity) return;

// Obtain the target entity from the input parameters.
var target = (Entity)executionContext.InputParameters["Target"];

Fontos, hogy mindig ellenőrizzük az adatok meglétét és típusát. A rekordjainkat jellemzően egy „Entity” típusú objektum reprezentálja, tehát a Dataverse-ben minden tábla sora egy Entity objektumként használható, ezért minden esetben cast-olnunk kell. Az „Entity” típusú objektumunk rendelkezik egy „Attrbiutes” property-vel, ami egy attribútum gyűjtemény kulcs-érték párokkal. Így tudjuk megszerezni például a kapcsolattartó rekordunkon szereplő cég azonosítóját is, amit lekérhetünk:

var accountRef = (EntityReference)target.Attributes["parentcustomerid"];

// Getting account with phone number
var columns = new ColumnSet("telephone1");
var account = orgService.Retrieve("account", accountRef.Id, columns);

A rendszerben „Lookup” típusú mezők itt „EntityReference” típusként jelennek meg és rendszerint tartalmazzák a kapcsolódó rekord azonosítóját és a tábla logikai nevét.

Egy „ColumnSet” objektummal tudjuk szűkíteni, hogy mely oszlopokat / mezőket / attribútumokat szeretnénk lekérni, ami performancia szempontból fontos lehet, mivel egy tábla több száz oszlopot is tartalmazhat. Jelen esetben nekünk csak a „telephone1” mezőre van szükségünk. Fontos, hogy itt az loszlop logikai nevére van szükségünk.

Sikeres lekérés esetén már kinyerhetjük a cég rekord telefonszámát és hozzárendelhetjük az eddigi módosításainkhoz, mivel a plugin-t majd PreOperation fázisban fogjuk regisztrálni, tehát a módosításaink még nincsenek mentve:

if (account.Attributes["telephone1"] == null) return;

var mainPhone = (string)account.Attributes["telephone1"];
target.Attributes["company"] = mainPhone;

Az egész plugin tartalma:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;

namespace kogerohu
{
    /// <summary>
    /// Copy company phone from related Account main phone column if exists when Account is changed.
    /// This operation should commit before data will be saved => PreOperation
    /// </summary>
    public class ContactCopyCompanyPhone : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext executionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            // Obtain the Organization Service factory service from the service provider
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService orgService = factory.CreateOrganizationService(executionContext.UserId);
            
            // The InputParameters collection contains all the data passed in the message request.
            var isTarget = executionContext.InputParameters.Contains("Target");
            var isTargetTypeEntity = executionContext.InputParameters["Target"] is Entity;

            if (!isTarget || !isTargetTypeEntity) return;

            // Obtain the target entity from the input parameters.
            var target = (Entity)executionContext.InputParameters["Target"];

            // If there is no related account do nothing
            if (!target.Attributes.ContainsKey("parentcustomerid")) return;

            var accountRef = (EntityReference)target.Attributes["parentcustomerid"];

            // Getting account with phone number
            var columns = new ColumnSet("telephone1");
            var account = orgService.Retrieve("account", accountRef.Id, columns);

            // If no main phone, do nothing
            if (account.Attributes["telephone1"] == null) return;

            var mainPhone = (string)account.Attributes["telephone1"];

            // Copy account's main phone to contact's company phone
            target.Attributes["company"] = mainPhone;

            // Since target goes further on the execution pipeline
            // And main stage is next, our data will be saved and
            // we don't have to save our data manually.
        }
    }
}

A projekt megtalálható a GitHub-omon ide kattintva. Az egyszerűség kedvéért minden eset nem lett lekezelve.

Lefordítás gépi kódra

Le kell fordítanunk a megírt kódunkat, mert csak DLL-ként tudjuk feltölteni. Ehhez viszont egy aláírást is létre kell hozzunk és a projekthez rendelnünk, mert máskülönben a feltöltés sikertelen lesz.

Projekt aláírása

Az aláíráshoz szükségünk van egy „.snk” kiterjesztésű kulcs generálására.

Ezt parancssorban az „sn -k fájlnév.snk” segítségével tudjuk elvégezni, esetemben „sn -k kogerohu.snk” lesz. Ha ez kész, akkor Visual Studio-ban a projektünkre jobb klikk > „Properties”.

A megnyíló ablakban bal oldalon válasszuk a „Signing” menüpontot és keressük meg a „Sign the assembly” jelölő négyzetet. Ezt követően a lenyíló menüben válasszok a „Browse”-t és tallózzuk be a generált név kulcsunkat:

Signing assembly in Visual Studio

Ne felejtsünk el menteni Ctrl + S billentyű kombinációval.

Projekt build-elése

Már tényleg csak pár kattintás, csinálhatjuk így is: a Solution Explorer-ben a plugin-t tartalmazó projektünkre jobb egérgombbal kattintva válasszuk ki a „Build” parancsot.

Build project in Visual Studio
Projekt build-elése

Ez létrehoz nekünk a projekt alatt egy DLL fájlt, ami a projekt alatt lévő „bin/Debug” mappába kerül. Esetemben „kogerohu.dll” lett. (Akinél nem jelenne meg a lentihez hasonlóan, az kapcsolja be a „Show All Files”-t.)

Debug folder with DLL in Visual Studio
Debug mappa a DLL-lel

Plugin regisztrálása

Plugin feltöltése a Plugin Registration Tool-lal vagy az XRM ToolBox Plugin Registration Tool-jával is elvégezhető, a menete és lényege azonos. Most a Plugin Registration Tool-on mutatom be.

Plugin Registration Tool letöltése

Az eszköz letölthető NuGet-ről. Ehhez mellékelek egy kis PowerShell szkriptet, ami minden a fejlesztéshez fontos eszközt letölt a számunkra. Futtassuk az alábbi kódot egy Power Shell ablakban:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$sourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$targetNugetExe = ".\nuget.exe"
Remove-Item .\Tools -Force -Recurse -ErrorAction Ignore
Invoke-WebRequest $sourceNugetExe -OutFile $targetNugetExe
Set-Alias nuget $targetNugetExe -Scope Global -Verbose

##
##Download Plugin Registration Tool
##
./nuget install Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool -O .\Tools
md .\Tools\PluginRegistration
$prtFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool.'}
move .\Tools\$prtFolder\tools\*.* .\Tools\PluginRegistration
Remove-Item .\Tools\$prtFolder -Force -Recurse

##
##Download CoreTools
##
./nuget install  Microsoft.CrmSdk.CoreTools -O .\Tools
md .\Tools\CoreTools
$coreToolsFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.CoreTools.'}
move .\Tools\$coreToolsFolder\content\bin\coretools\*.* .\Tools\CoreTools
Remove-Item .\Tools\$coreToolsFolder -Force -Recurse

##
##Download Configuration Migration
##
./nuget install  Microsoft.CrmSdk.XrmTooling.ConfigurationMigration.Wpf -O .\Tools
md .\Tools\ConfigurationMigration
$configMigFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.ConfigurationMigration.Wpf.'}
move .\Tools\$configMigFolder\tools\*.* .\Tools\ConfigurationMigration
Remove-Item .\Tools\$configMigFolder -Force -Recurse

##
##Download Package Deployer 
##
./nuget install  Microsoft.CrmSdk.XrmTooling.PackageDeployment.WPF -O .\Tools
md .\Tools\PackageDeployment
$pdFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.PackageDeployment.Wpf.'}
move .\Tools\$pdFolder\tools\*.* .\Tools\PackageDeployment
Remove-Item .\Tools\$pdFolder -Force -Recurse

##
##Remove NuGet.exe
##
Remove-Item nuget.exe

A letöltött szoftverek egy „../Tools/” mappába kerülnek, amin belül már a „Plugin Registration” mappában lévő exe-vel indíthatjuk is.

A program betöltését követően be kell jelentkezzünk a környezetünkre. Ha ez sikeresen megtörtént, akkor az alábbi képnek megfelelő ablakot kell lássunk a rendszerben található összes plugin-nel. Válasszuk ki a „Register > Register new Assembly”-t.

Plugin Registration Tool
Plugin Registration Tool

A következő ablakban csak tallózzuk be a „Debug” mappából a DLL-t, máshoz ne nyúljunk:

Register new assembly in Plugin Registration Tool
Assembly regisztrálása

Nagyon fontos, hogy ha itt valamelyik plugin nincs kipipálva, de már a rendszerben létezik, akkor regisztrálást követően eltávolításra kerül!

Kattintsunk a „Register Selected Plugins” gombra. Siker esetén az összefoglaló ablakban jelen esetben ezt kell kapjuk:

Regisztrált elemek
Regisztrált elemek

Ezt követően megjelenik az Assembly és a Plugin a fő ablak listájában.

Plugin step létrehozása

A sikeres regisztrációval az assembly-nk és a plugin-ünk felkerült a rendszerbe, de még nem tudja, hogy milyen eseményre kell lefusson. Ezért létre kell hoznunk ún. „plugin step-eket”, amiknél definiálhatjuk, hogy milyen eseményre és milyen módon fusson le a feltöltött logikánk. Ehhez keressük kia plugin-ünket, majd jobb klikk rá és válasszuk a „Register New Step” parancsot.

Register plugin step
Step regisztrálása

A felugró ablakot a példánkra a lentinek megfelelően töltsük ki:

Plugin step registration
Step regisztrálása

Fontos, hogy „Update” eseményre szeretném, hogy lefusson a telefonszám ellenőrzés és másolás egy „contact” típusú rekord esetén, de csak akkor, ha a „parentcustomerid” logikai névvel rendelkező kereső (lookup) mező frissítésre kerül. Ne felejtsük el, hogy ezt a műveletet „PreOperation” fázisban szeretnénk elvégezni szinkron módon, így hiba esetén minden módosításunk visszaállításra kerül és a felhasználónak is vissza lehet dobni hibaüzenetet. Ha minden rendben, akkor zárjuk be a „Register New Step” gombbal.

Ha ez sikeres volt, akkor a step megjelenik a listában:

plugin step
Plugin step

Innentől fogva ez futni fog, ha egy kapcsolattartón módosítjuk a céget.

Hozzáadás solution-höz

Fontosnak tartom megemlíteni, hogy hol és hogyan találjuk meg a maker portálon a feltöltött és létrehozott elemeinket és hogyan tudjuk hozzáadni egy solution-höz. Látogassunk el a make.powerapps.com-ra, válasszuk ki jobb felül a környezetünket. Hozzunk létre vagy válasszunk ki egy létező solution-t, majd a képen látható módon be tudjuk tallózni a létező elemek közül:

Plugin assembly és step hozzáadása megoldáshoz
Plugin assembly és step hozzáadása megoldáshoz

Mikor kellhet plugin?

Az egyedi kódon kívül ún. workflow-t is használhatunk egyedi logikák készítésére, ami ráadásul egy no-code eszköz a rendszerben, viszont újabb eszköz a Power Automate-en belül a „cloud flow”, amely egy no-code low-code eszköz, tehát nem igazán igényel programozási ismereteket.

Sajnos mindegyiknek meg vannak a maga korlátai, például a cloud flow aszinkron módon fut le – azaz nem azonnal -, így például nem tudunk a felhasználónak hibaüzenetet dobni, megvárni, hogy mi lett az eredménye.

Mindkét eszköz, ha egyre bonyolodó logikát tartalmaz könnyen áttekinthetetlenné válik. Az kijelenthető, hogy a no-code low-code eszközök egyedi üzleti logikára általában kevésbé produktívan készíthetők el a plugin kódolásához képest.

Néhány szempont, hogy mikor kellhet plugin a Dataverse-ben:

  • Ha az ügyfél nem ragaszkodik a no-code low-code eszközökhöz, azaz később a folyamatok változása esetén szívesen kerít fejlesztőt a módosítások elvégzésére…
  • Ha egyedi logikát alkalmaznánk számításokra, amit egy mezőbe/oszlopba mentenénk például adószám validáció vagy például különböző rekordokról összeszedni és kombinálni egy adott mező tartalmát stb.
  • Ha egyedi eseményre végeznénk műveleteket (ezt nem fedte le ez a bejegyszés)
  • Ha szeretnénk hibaüzenetet visszaadni a felhasználónak
  • Ha nem akarunk a logika fejlesztés során korlátokba ütközni később
  • A megírt logikát szeretnénk kódban írt tesztekkel is tesztelni, mert előírás a teszt lefedettség

Összefoglalás

A teljes bejegyzésben nagyon tömören ismertetve lett az „event framework”, ami által eseményekhez kötve tudunk egyedi logikát is futtatni. Megnéztük, hogy hogyan tudunk Visual Studio-ban egy projektet létrehozni, hogy plugin-t készítsünk és hogy ehhez milyen SDK szükséges. Megnéztük, hogy hogyan készítsük el a DLL-t és töltsük fel a Dataverse-be, majd kössük egy entitáshoz és eseményhez. Végül pedig hol találjuk és tudjuk a plugint solutionbe csomagolni.

Aki tovább szeretne elmélyülni a témában, annak néhány link:

  1. Event execution pipeline
  2. Use plugins to extend business processes

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük