Implement business logic with Dataverse Custom API

In march 2021 Microsoft announced that the Dataverse Custom API reaches General Availability and therefore it's about time to take a look at those new extension points for the Power Platform. The concept that allows encapsulating multiple steps of business-logic under a custom operational contract is available since Dynamics 2013 with custom process actions and significantly improved reusability across the application as consumption of those actions was available from scripts in the frontend, plugins/codeactivity on the serverside, as well as workflow steps of the workflow designer. So let's see how custom APIs can make the business-logic shine.

What are custom APIs?

The definition of Custom APIs consists of metadata about the API itself, its optional request parameters as well as response properties. This information is stored in dataverse tables and is solution aware allowing it to be easily managed in ALM scenarios.

Affected Tables

Getting started with an example

Lets dive into a sample to see how this all works out. Imagine we have have setup a table for invoices with a total amount and each of those invoices can have line items with an individual amount.

Table relationship of invoice and its line items

Requirements

  • The new API should be bound to the invoice table and accept an input collection of line items that should be added to that invoice.
  • Once the update of given line items succeeded, the total amount has to be updated on the invoice record and must be returned to the caller.
  • The Action may only be called by users having create privilege on invoices.
  • In case of failures the whole operation needs to be rolled back, to prevent an inconsistent state of data (e.g. only some of the line item updates succeeded).

Configuration

Lets start with the metadata describing the API's appearance. The easiest way is use the XrmToolBox plugin CustomApiManager created by David Rivard.

From the screenshot below you can see the configuration:

  • API Name: new_AddInvoiceLineItems
  • Plugin Type: BlogDemo.AddInvoiceLineItemsPlugin
  • Execute Privilege Name: prvCreatenew_Invoice
  • Request Parameter: InvoiceLineItems
  • Response Property: TotalAmount

Custom API Metadata

Implementation

Once we setup the API its time to write the plugin code so that we can wire it up later on in the metadata.

using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using System; using System.Linq; using static Microsoft.Xrm.Sdk.Query.ConditionOperator; namespace BlogDemo { public class AddInvoiceLineItemsPlugin : IPlugin { public void Execute(IServiceProvider serviceProvider) { var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); var service = factory.CreateOrganizationService(context.UserId); if (!(context.InputParameters.TryGetValue("Target", out var target) && context.InputParameters.TryGetValue("InvoiceLineItems", out var lineItems))) { throw new InvalidPluginExecutionException("Invoice or Line Items are not present."); } var invoice = target as EntityReference; var invoiceLineItems = lineItems as EntityCollection; // Retrieve all invoice item that were passed along with request parameter var lineitems = service.RetrieveMultiple(new QueryExpression("new_invoicelineitem") { Criteria = new FilterExpression { Conditions = { new ConditionExpression("new_invoicelineitemid", In, invoiceLineItems.Entities.Select(x => x.Id).ToArray()) } } }).Entities; // Set the lookup of each line item to the invoice reference lineitems.Select(x => new Entity("new_invoicelineitem", x.Id) { Attributes = { ["new_invoiceid"] = invoice } }).ToList() .ForEach(service.Update); // Calculate the total amount of all line items including those that might have been previously added. var lineItemAmount = new Money(service.RetrieveMultiple(new QueryExpression("new_invoicelineitem") { ColumnSet = new ColumnSet("new_amount"), Criteria = new FilterExpression { Conditions = { new ConditionExpression("new_invoiceid", Equal, invoice.Id), new ConditionExpression("new_amount", NotNull), } } }).Entities .Sum(x => x.GetAttributeValue<Money>("new_amount").Value)); // Uncomment this line if you want to prove that previous service calls are rolled back. //throw new InvalidPluginExecutionException("Are we correctly rolling back"); // Update total amount on invoice service.Update(new Entity(invoice.LogicalName, invoice.Id) { ["new_totalamount"] = lineItemAmount }); context.OutputParameters.Add("TotalAmount", lineItemAmount); } } }

When the plugin dll is registered its time to update the custom api's plugin type. Remember we do not register a step for the plugin type as it is configuration option in the custom api record (see red box on screenshot above).

Testing

To test our code we use the F12 tool's console window in the browser to kickstart the action with the help of Xrm.WebApi client object model.

First we create an invoice and two line items that are not yet associated. The action is described with the constructor function AddInvoiceLineItems. You can find find more about this admittedly complex parameter configuration in the official docs.

let invoiceIndex = 1; let invoiceItemIndex = 1; var invoiceId = await Xrm.WebApi.createRecord("new_invoice", { new_name: "invoice " + invoiceIndex++, }); let invoiceItemId1 = await Xrm.WebApi.createRecord("new_invoicelineitem", { new_name: "invoice" + invoiceIndex + " lineitem " + invoiceItemIndex++, new_amount: 100 }); let invoiceItemId2 = await Xrm.WebApi.createRecord("new_invoicelineitem", { new_name: "invoice" + invoiceIndex + " lineitem " + invoiceItemIndex++, new_amount: 100 }); function AddInvoiceLineItems(invoiceId, invoiceLineItems) { return { entity: { entityType: "new_invoice", id: invoiceId, }, InvoiceLineItems: invoiceLineItems, getMetadata() { return { operationName: "new_AddInvoiceLineItems", boundParameter: "entity", parameterTypes: { entity: { typeName: "mscrm.new_invoice", structuralProperty: 5, }, InvoiceLineItems: { typeName: "Collection(mscrm.crmbaseentity)", structuralProperty: 4, }, }, operationType: 0, }; }, }; }; let response = await Xrm.WebApi.online.execute( new AddInvoiceLineItems(invoiceId.id, [invoiceItemId1, invoiceItemId2]) ); let actionReponse = await response.json(); console.log("Total Invoice Amount is: " + actionReponse.TotalAmount);

OUTPUT: Total Invoice Amount is: 200

The Xrm.WebApi.online.execute function executes the action and sends a POST request to the Dataverse Web API.

POST: https://<environnmenturl>/api/data/v9.0/new_invoices(b0ddfe3d-7db4-eb11-8236-0022487eee10)/Microsoft.Dynamics.CRM.new_AddInvoiceLineItems { "entity": { "@odata.type": "Microsoft.Dynamics.CRM.new_invoice", "new_invoiceid": "b0ddfe3d-7db4-eb11-8236-0022487eee10" }, "InvoiceLineItems": [ { "@odata.type": "Microsoft.Dynamics.CRM.new_invoicelineitem", "new_invoicelineitemid": "b1ddfe3d-7db4-eb11-8236-0022487eee10" }, { "@odata.type": "Microsoft.Dynamics.CRM.new_invoicelineitem", "new_invoicelineitemid": "b2ddfe3d-7db4-eb11-8236-0022487eee10" } ] }

As you can see it was very straight forward to implement the logic inside a plugin and call it from client-side scripts. You can find an unmanaged solution of the demo here.

Wrapping up

The new approach of Custom APIs has significant advantages:

  • Can be unbound (global) or bound to a table/entity
  • Custom API definitions are solution aware components included in a solution through a set of folders and XML documents.
  • Can designate that a user must have a specific privilege to call the message.
  • Can be marked private if they are not intended to be used by anyone else.
  • With Custom API the message creator simply associates their plug-in type with the Custom API to provide the main operation logic.

Blog post written by

Marius Pothmann

Marius Pothmann is a Microsoft certified solution architect focusing on the Power Platform and cloud-native application development on Azure. Working on the Microsoft stack for over a decade gave him the opportunity to deep dive into many building blocks of successful enterprise application architectures like Dynamics 365, SQL Server, ADFS, ASP.NET (Core), SPA-based Web Apps, WPF/WinForms. If you think he can assist in solving your challenges drop us an e-mail and we will get back to you right away.