Sometimes for dataverse you will want to aggregate some of the API operations together for an integration scenario where you want to provide a friendly API that is easy for an external consumer to use. They might not want to care about all of the internal relationships between entities and optionsets and the dataverse internals.

Lets imagine you have an parent/child relationship from an order to an order line in your dataverse and those entities may also reference other entities for some fields.

If you are using APIM you can create an aggregator proxy API which can combine multiple calls to dataverse and transform them to a friendly format. In this way I can just query my API with an order number and get back a json response which shows the order and its order lines in 1 call to APIM.

Conceptually your solution would look like the below diagram.

Inside the API you would want to implement a policy which makes a call to Dataverse to get the order. You would then get the order lines and merge them together into a single response. A sequence diagram representing this would look like the below.

You can also use features on the Dataverse API calls to expand entity relationships, restrict the number of fields returned and expand optionsets.

In my API call I can just use the below url to do an HTTP GET. I am also passing an OAuth token to the API which has access to the dataverse. Note that with APIM you would also have options to change the authentication for the client if your solution needed it. In my case the client has a service principal which has access to Dataverse so I can flow through the token.

https://{{API_HOST}}/acme/api/orders/v1/ORD-02185-V5N3G7

The response I get back contains a json object which contains details from the order and then an array for the order lines with some details from each order line. For the purposes of the demo I am just flowing through a couple of id properties but you will see later it is easy to add more fields.

In the policy I am able to use the Send-Request action a couple of times to make calls out to dataverse to query the data I need and the Send-Request gives me the responses in variables and I can then use the Set-Variable action to merge the 2 responses together into a single json. I then use a liquid transform which allows me to map the data from the not very friendly dataverse format to something friendly for the client user.

If we take a look at the liquid part of the solution you can see its quite easy to convert the dataverse data. Because I merged the 2 dataverse responses together I can just access then as the order line and order properties on the body.



<set-body template="liquid">
        {            
            "order_id":"{{body.order.acme_orderid}}",
            "order_lines":[
            {% JSONArrayFor orderline in body.orderLines.value %}
                {
                    "order_line_id" : "{{orderline.acme_orderproductid}}",
                    "order_id" : "{{orderline['_acme_order_value@OData.Community.Display.V1.FormattedValue']}}"
                }
            {% endJSONArrayFor %}
            ]         
        }
        </set-body>

If I want to add any additional fields to the output I can just add properties to the output message I am creating and address them in the liquid filters. In the queries I made to dataverse I also added the $expand parameters in a couple of cases so my order would have some nested objects like a link to its account so it is quite easy for me to build a detailed but friendly response for my API client.

Policy

The full policy I used is below and I have put some comments on each step to help explain how it works.



<policies>
    <inbound>
        <base />

        <!-- Extract the order external id so we can use it in the policy -->
        <set-variable name="orderExternalId" value="@(context.Request.MatchedParameters["id"])" />


        <!-- Save the auth header to a variable so we can use it on multacmee requests -->
        <set-variable name="authHeader" value="@(context.Request.Headers.GetValueOrDefault("Authorization"))" />


        <!-- Format the url for querying the order -->
        <set-variable name="orderQueryUrl" value="@{
            var url = "https://devacmecrm.crm.dynamics.com/api/data/v9.2/acme_acmeorders?";                        
            url += "$filter=acme_orderid eq '" + (string)context.Variables["orderExternalId"] + "' and statecode eq 0 and statuscode eq 1";            
            url += "&$expand=acme_Account($expand=acme_PaymentTerms,owninguser($select=ownerid,fullname,domainname))";            

            return url;
        }" />


        <!-- Send the request to dataverse to get the order -->
        <send-request mode="new" response-variable-name="crm_order_response" timeout="20" ignore-error="false">
            <set-url>@((string)context.Variables["orderQueryUrl"])</set-url>
            <set-method>GET</set-method>
            <set-header name="Authorization" exists-action="override">
                <value>@((string)context.Variables["authHeader"])</value>
            </set-header>
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <!-- This will expand the option sets -->
            <set-header name="Prefer" exists-action="override">
                <value>odata.include-annotations="*"</value>
            </set-header>
        </send-request>


        <!-- Using the $filter above we get an array, this will extract the single order object and check there is just 1 -->
        <set-variable name="crm_order" value="@{
            var orderResponse = (IResponse)context.Variables["crm_order_response"];
            JObject orderResponseObject = JObject.Parse(orderResponse.Body.As<string>(preserveContent: true));
            var crmResponseItems = (JArray)orderResponseObject["value"];  
            if(crmResponseItems.Count == 0)
            {                    
                throw new Exception("There are no records matching this id");                                        
            }
            else if(crmResponseItems.Count > 1)
            {
                throw new Exception("There is more than 1 record matching this id");
            }
            else
            {
                foreach (var item in crmResponseItems)
                {
                    return item;
                }   
            }  

            return null;                      
        }" />


        <!-- Extract the guid from the Order so we can use it for querying order items matching that order -->
        <set-variable name="orderInternalId" value="@{
            var order = (JObject)context.Variables["crm_order"];            
            return order["acme_acmeorderid"].ToString();
                                  
        }" />


        <!-- Format the url for the order line query -->
        <set-variable name="orderQueryLineUrl" value="@{
            var url = "https://devacmecrm.crm.dynamics.com/api/data/v9.2/acme_acmeorderproducts?";                        
            url += "$filter=_acme_order_value eq '" + (string)context.Variables["orderInternalId"] + "' and statecode eq 0 and statuscode eq 1";
            url += "&$expand=acme_PriceFile($expand=acme_Location,acme_ProductName)";            

            return url;
        }" />


        <!-- Send a get request to dataverse to query the order lines -->
        <send-request mode="new" response-variable-name="crm_order_lines" timeout="20" ignore-error="false">
            <set-url>@((string)context.Variables["orderQueryLineUrl"])</set-url>
            <set-method>GET</set-method>
            <set-header name="Authorization" exists-action="override">
                <value>@((string)context.Variables["authHeader"])</value>
            </set-header>
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <!-- This will expand the option sets -->
            <set-header name="Prefer" exists-action="override">
                <value>odata.include-annotations="*"</value>
            </set-header>
        </send-request>


        <!-- Merge the 2 responses into a combined json object so we can use a liquid transform later -->
        <set-body template="none">@{
            var orderResponse = (JObject)context.Variables["crm_order"];
            var orderLineResponse = (IResponse)context.Variables["crm_order_lines"];                
                
            JObject response = new JObject();
            response.Add(new JProperty("order", orderResponse));
            response.Add(new JProperty("orderLines", orderLineResponse.Body.As<JObject>()));
            return response.ToString();
        }</set-body>


        <!-- We need to set the content type here so when we do the liquid map in the next step the policy sees the request as json -->
        <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
        </set-header>


        <!-- Transform the data to create a friendly response -->
        <set-body template="liquid">
        {            
            "order_id":"{{body.order.acme_orderid}}",
            "order_lines":[
            {% JSONArrayFor orderline in body.orderLines.value %}
                {
                    "order_line_id" : "{{orderline.acme_orderproductid}}",
                    "order_id" : "{{orderline['_acme_order_value@OData.Community.Display.V1.FormattedValue']}}"
                }
            {% endJSONArrayFor %}
            ]         
        }
        </set-body>


        <!-- Return the response to the caller -->
        <return-response>
            <set-status code="200" />
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-body>@(context.Request.Body.As<string>(preserveContent: true))</set-body>
        </return-response>

    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

 

Buy Me A Coffee