Home / Nieuws / Een custom API maken en deze aanroepen met TypeScript
21 april 2022
Door: Wilmer Alcivar

Extend Microsoft D365 Finance and Operations with Dynamics 365 Commerce

Custom API

Het is een jaar geleden dat de aangepaste API live ging en ik heb wat berichten gelezen en video’s van de community bekeken over hoe waardevol custom API’s zijn. Maar tot vandaag was het gewoon “oh, er is iets nieuws, ik moet dat binnenkort eens uitproberen”.

Twee weken geleden kregen we een nieuw verzoek in een project, dus ik had eindelijk de kans om het uit te proberen en het potentieel te zien, het verzoek was:

“Als sales manager wil ik een order aanmaken op basis van een offerte. Hiermee moeten de gegevens uit de Offerte en de Offerteregels naar de Order en Orderregels worden gekopieerd. Ter verduidelijking, deze entiteiten zijn op maat gebouwd, omdat de business case complexer was dan de Out of the Box-functionaliteit.  Vanwege de beperkingen van de native productcatalogus was het beter om te kiezen voor op maat gemaakte entiteiten.

Ik ben enthousiast omdat we met de custom API een aantal meer geavanceerde functies krijgen, zoals:

  • Private Custom API: Hiermee is de Custom API alleen beschikbaar voor de eigenaar en de gebruikers met wie de eigenaar deze deelt. Microsoft zelf gebruikt deze functionaliteit om de interne acties en functies te verbergen. Dit voegt een extra beveiligingslaag toe.
  • API-type: We kunnen onze aangepaste API definiëren om als Function of als Action te worden uitgevoerd. Voor het type Function moeten we het uitvoeren met behulp van de GET-methode en voor het type Action kunnen we het uitvoeren met behulp van de POST-methode. Met deze typen kunnen we de aangepaste API rechtstreeks vanuit een Power Automate Cloud Flow aanroepen vanuit een bound action.
  • Codebenadering: Custom API ondersteunt het gebruik van code en daarmee kunnen we zoveel aanpassen als we willen.

Wanneer we een aangepaste API willen maken, hebben we drie componenten nodig:

  • De plugin
  • De aangepaste API- en parametersrecords (we kunnen deze maken met behulp van make.powerapps.com)
  • Een manier om de aangepaste API aan te roepen, het kan een webresource, een cloudstroom of een workflow zijn.

 

Maak de plugin

Hoe ziet de eerste stap er uit? Ik heb geprobeerd de plug-in zo eenvoudig mogelijk te houden en alle zware logica te delegeren aan andere procedures en functies.

public class CreateOrderFromQuote : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);

            tracingService.Trace("Start Custom API createOrderFromQuote");

            EntityReference quoteRef = new EntityReference(context.PrimaryEntityName, context.PrimaryEntityId);
            var quoteHandler = new Core.Handlers.QuoteHandler(tracingService, service);
            quoteHandler.createOrderFromQuote(quoteRef, context);

            tracingService.Trace("End Custom API createOrderFromQuote");
        }
    }

De procedure Execute roept de procedure createOrderFromQuote aan die zich in een andere class bevindt.  Dit is de belangrijkste methode omdat het alle andere functies aanroept.

Wat ik wil benadrukken in de procedure createOrderFromQuote is het object OrganizationRequestCollection, omdat we met dit object de order en de regels ervan in één request kunnen uitvoeren. Dit is veel beter omdat de prestaties veel hoger is dan als we de Order en zijn Lines één voor één creëren.

En dit is de procedure die de Custom API- output parameters vult:

 public void CreateOrderFromQuote(EntityReference quoteRef, IPluginExecutionContext context)
        {
            _tracingService.Trace("Start QuoteHanlder createOrderFromQuote");

            OrganizationRequestCollection requestCollection = new OrganizationRequestCollection();

            Entity order = _CreateOrderFromQuote(quoteRef, requestCollection);
            _tracingService.Trace($"Order with ID {order.Id} added to Request Collection");

            EntityCollection quoteLinesColl = GetQuoteLinesByQuoteId(quoteRef);
            _tracingService.Trace($"quoteLinesColl : {quoteLinesColl.Entities.Count}");

            requestCollection = CreateOrderLinesFromCollection(quoteLinesColl, requestCollection, order);
            _tracingService.Trace($"requestCollection : {requestCollection.Count}");

            _tracingService.Trace($"Before executeBatchRequest");
            Helpers.Common.executeBatchRequest(_service, _tracingService, requestCollection);
            context.OutputParameters["createOrderSuccess"] = order.Id != Guid.Empty;
            context.OutputParameters["resultMessage"] = "this is a test";
            context.OutputParameters["orderRef"] = order.ToEntityReference();

            _tracingService.Trace("End QuoteHanlder createOrderFromQuote");
        }

Nu heb ik in het volgende stukje code twee functies die een handlerfunctie aanroepen met de naam createEntityfromMapping.  In deze functie is er nog een ander object dat ik zou willen benadrukken, het  is echt een verborgen juweeltje dat ons leven veel gemakkelijker zal maken. Het object is InitializeFromRequest en stelt ons in staat om “In Memory” een record te maken met dezelfde veldwaarden van de bronrecord, en hiervoor gebruikt het de Field Mapping Functionality tussen de twee entiteiten.  Als in de toekomst een nieuw veld moet worden gekopieerd, hoeft de gebruiker alleen maar een nieuwe Field Mapping te maken met behulp van de relatie tussen de twee entiteiten. Het is niet nodig om de code bij te werken, hoe cool is dat!

private Entity _CreateOrderFromQuote(EntityReference quoteRef, OrganizationRequestCollection requestCollection)
        {
            Entity order = Helpers.Common.createEntityfromMapping(_service, quoteRef, "dp_order", TargetFieldType.All);
            order.Id = Guid.NewGuid();
            requestCollection.Add(new CreateRequest() { Target = order });
            return order;
        }
 private OrganizationRequestCollection CreateOrderLinesFromCollection(EntityCollection quoteLinesColl, OrganizationRequestCollection requestCollection, Entity order)
        {
            foreach (var quoteLine in quoteLinesColl.Entities)
            {
                _tracingService.Trace($"quoteLineId : {quoteLine.Id}");
                Entity orderLine = Helpers.Common.createEntityfromMapping(_service, quoteLine.ToEntityReference(), "dp_orderline", TargetFieldType.All);
                orderLine.Attributes.Add("dp_orderid", order.ToEntityReference());
                requestCollection.Add(new CreateRequest() { Target = orderLine });
            }
            return requestCollection;
        }
 public static Entity createEntityfromMapping(IOrganizationService service, EntityReference sourceEntityRef, String targetEntityName, TargetFieldType targetFieldType)
        {
            InitializeFromRequest initializeFromRequest = new InitializeFromRequest();
            initializeFromRequest.EntityMoniker = sourceEntityRef;
            initializeFromRequest.TargetEntityName = targetEntityName;
            initializeFromRequest.TargetFieldType = targetFieldType;
            InitializeFromResponse initializeFromResponse = (InitializeFromResponse)service.Execute(initializeFromRequest);
            return initializeFromResponse.Entity;
        }

Met de volgende functie kan ik alle offerteregels ophalen in een collectie die ik later zal gebruiken  om de orderregels te maken:

private EntityCollection GetQuoteLinesByQuoteId(EntityReference quoteRef)
        {
            string fetchXML = $@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
              <entity name='dp_quoteline'>
                <attribute name='dp_quotelineid' />
                <filter type='and'>
                  <condition attribute='dp_quoteid' operator='eq' uitype='dp_quote' value='{quoteRef.Id}' />
                </filter>
              </entity>
            </fetch>";

            return Helpers.Common.getDatabyFetchXML(_service, fetchXML);
        }

Vervolgens maak ik in de volgende functie gebruik van de verzameling offerteregels uit de vorige functie om alle orderregels te maken.  Weer maak ik gebruik van het object InitializeFromRequest, dat de Field Mapping tussen de twee entiteiten (quoteregels en orderregels) gebruikt:

private OrganizationRequestCollection CreateOrderLinesFromCollection(EntityCollection quoteLinesColl, OrganizationRequestCollection requestCollection, Entity order)
        {
            foreach (var quoteLine in quoteLinesColl.Entities)
            {
                _tracingService.Trace($"quoteLineId : {quoteLine.Id}");
                Entity orderLine = Helpers.Common.createEntityfromMapping(_service, quoteLine.ToEntityReference(), "dp_orderline", TargetFieldType.All);
                orderLine.Attributes.Add("dp_orderid", order.ToEntityReference());
                requestCollection.Add(new CreateRequest() { Target = orderLine });
            }
            return requestCollection;
        }

Met behulp van de handlerfunctie createEntityfromMapping hebben we de Order en de regels “In Memory” gemaakt. Nu kunnen we de OrganizationRequestCollection verwerken met een andere handlerfunctie genaamd ExecuteBatchRequest. Deze functie maakt gebruik van het executeMultipleRequest object en hiermee worden de orders en de regels in slechts één aanvraag aangemaakt. Nadat het verzoek met succes is voltooid, kunnen we de aangepaste API-uitvoerparameters invullen:

public static void ExecuteBatchRequest(IOrganizationService service, ITracingService tracingService, OrganizationRequestCollection requestCollection, int split = 50)
        {
            String exceptionMessage = String.Empty;
            List<List<OrganizationRequest>> splittedLists = requestCollection.ToList().ChunkBy(split);
            tracingService.Trace($"Splitted {requestCollection.Count} into {splittedLists.Count} List with split setting of {split}");
            int i = 1;
            foreach (List<OrganizationRequest> listRequests in splittedLists)
            {
                OrganizationRequestCollection newRequestCollection = new OrganizationRequestCollection();
                newRequestCollection.AddRange(listRequests);
                ExecuteMultipleRequest execRequest = new ExecuteMultipleRequest()
                {
                    Settings = new ExecuteMultipleSettings()
                    {
                        ReturnResponses = true,
                        ContinueOnError = true
                    },
                    Requests = newRequestCollection
                };
                try
                {
                    tracingService.Trace($"Execute Multiple Request {i} of {splittedLists.Count}");
                    ExecuteMultipleResponse responseWithResults = (ExecuteMultipleResponse)service.Execute(execRequest);
                    tracingService.Trace($"Multiple Request Executed. Is faulted : {responseWithResults.IsFaulted}");
                    i++;
                }
                catch (Exception ex)
                {
                    tracingService.Trace($"Error {ex}");
                    exceptionMessage += ex.Message;
                }
                finally
                {
                    if (!String.IsNullOrEmpty(exceptionMessage))
                    {
                        tracingService.Trace($"Exception: {exceptionMessage}");
                    }
                }
            }
        } 

Compileer het project en we uploaden de assembly met behulp van de plugin registration tool met de optie “Register New assembly”. Voor deze actie hoeven we geen plug-in stappen te maken.

 

Maak de Custom API

Laten we nu eens kijken wat we bij de tweede stap moeten doen. In een Solution doen we het volgende:

We selecteren Custom API en vervolgens wordt een klassiek formulier geopend waarin we de Custom API maken.

We geven een unieke naam aan de Custom API en in mijn geval wilde ik deze koppelen aan de custom entiteit dp_quote. Als je de Custom API vanuit een werkstroom wilt aanroepen, moet het veld ‘ingeschakeld voor werkstroom’ worden ingesteld op Ja. En ten slotte moeten we in het laatste veld “Plugin Type” de assembly selecteren die in de vorige stap is gegenereerd:

Vervolgens moeten we de parameters van de Custom API maken.  In dit geval heb ik 3 output parameters, die ik in de plug-in heb gedefinieerd. Om de parameters te maken, moeten we het volgende doen:

We moeten Custom API Response Property selecteren en vervolgens wordt een klassiek formulier geopend waarmee we de parameter kunnen maken.

We moeten een unieke naam geven aan de parameter en we moeten deze koppelen aan de aangepaste API die is gemaakt in de vorige stap. Let op: het is belangrijk om het juiste gegevenstype aan de parameter te geven:

Maak een component om de Custom API aan te roepen:

De laatste stap is het maken van een manier om de Custom API aan te roepen. It kan een workflow zijn, een cloud flow of in mijn geval een button. Ik heb een knop  gemaakt die  een JavaScript-functie aanroept en binnen deze functie roepen we de Custom  API aan.

In mijn geval heb ik een TypeScript-project  gemaakt dat is geconfigureerd om automatisch de JavaScript-bestanden te genereren wanneer het project wordt gecompileerd.

En om het wiel niet opnieuw uit te vinden, gebruikte ik de Xrm ToolBox-plug-in genaamd “Dataverse REST Builder” om de JavaScript-code te genereren om de Custom API aan te roepen:

Zodra de “Dataverse REST Builder” is geopend, moeten we het volgende doen:

We selecteren de optie “Custom API uitvoeren”.

Vervolgens moeten we aan de rechterkant van het scherm de tabel en de Custom API selecteren die we zojuist hebben gemaakt. In mijn geval selecteer ik dp_quote als de tabel en dp_createorderfromquote als de Custom API, ten slotte moeten we het tabblad Xrm.WebAPI clicken

En dan zie je JavaScript-code die lijkt op de volgende:

Als je een JavaScript-project hebt, kopieer en plak je de code gewoon in een JS-webresource  van je Dataverse omgeving, maar in mijn geval moest ik het eerst een beetje veranderen, want zoals ik het al eerder zei, had ik een TypeScript-project:

export async function approve (primaryControl: Xrm.FormContext) {
        console.log('Function approveReject called');
        if (!primaryControl) {
            console.log('Primary Control not present, abort');
        }
        const formContext = primaryControl;
        let recordId = formContext.data.entity.getId();
            recordId = recordId.replace("{","").replace("}","");
            callCustomAPICreateOrderAndOrderLines(recordId);

        formContext.data.refresh(true);
    }

 export function callCustomAPICreateOrderAndOrderLines(quoteId : string) {
        console.log('Function callCustomAPICreateOrderAndOrderLines called');
        return new Promise<Xrm.ExecuteResponse>(function (resolve, reject) {
            const execute_dp_createorderfromquote_Request = {
                // Parameters
                entity: { entityType: "dp_quote", id: quoteId }, // entity

                getMetadata: function () {
                    return {
                        boundParameter: "entity",
                        parameterTypes: {
                            entity: { typeName: "mscrm.dp_quote", structuralProperty: 5 }
                        },
                        operationType: 0, operationName: "dp_createorderfromquote"
                    };
                }
            };

            Xrm.WebApi.online.execute(execute_dp_createorderfromquote_Request).then(
                function success(response) {
                    if (response.ok) {
                        response.json().then(function (results) {
                            const createOrderSuccess = results["createOrderSuccess"];
                            const dp_orderid = results["orderRef"]["dp_orderid"];
                            const odatatype = results["orderRef"]["@odata.type"];
                            const resultMessage = results["resultMessage"];
                        });
                        resolve(response);
                    }
                }
            ).then(function (responseBody) {
                var result = responseBody;
                console.log(result);
            }).catch(function (error) {
                console.log(error.message);
                reject(error);
            });
        });
    }

En na het compileren van het TypeScript-project wordt uiteindelijk het JavaScript-bestand automatisch gegenereerd, dus ik heb die code gepubliceerd als een JS-webresource in mijn Dataverse-omgeving.

 

De JavaScript-functie aanroepen vanaf een knop

De laatste stap is om de JavaScript-functie aan  te roepen vanaf een knop.  We zullen de Ribbon Workbench-tool gebruiken. Hou in gedachten dat we een CRM-parameter genaamd primaryControl moeten doorgeven  aan de JavaScript-functie.

Nu de knop is gemaakt, kunnen we eindelijk onze aangepaste API testen! In mijn model driven app heb ik een offerte geopend en druk ik op de knop Approve die we zojuist hebben gemaakt:

Nu worden het orderrecord en de orderregels gemaakt:

Dit is de order:

En dit zijn de regels:

Conclusie

Custom API is inderdaad de nieuwe en geavanceerde manier om de Custom Messages op Dynamics 365 / Dataverse te registreren met de code benadering. In de toekomst zal ik Custom API gebruiken, en geen Custom Action, de volgende keer dat ik “Custom Action” -functionaliteit nodig heb.

Het is ook belangrijk om te vermelden dat we aandacht moeten besteden aan onze server-side code. Aangezien de Custom API een plug-in is en we de performance zo goed mogelijk moeten maken, niet alleen vanwege de uitvoeringslimiet van 2 minuten, maar ook omdat het een goede gewoonte is om een server-side code performant te maken. Daarom heb ik in de server side code een snelle en efficiënte manier gebruikt om meerdere records in één verzoek te maken.

Ik wil Niels Minnee bedanken die me heeft geholpen bij het toepassen van deze nieuwe coole feature om de requirements op de best mogelijke manier op te lossen.