oData actions

1    Introduction

I have been experimenting with Data Entity oData actions and had to overcome a few issues. Overall it is easy to implement them and they can be used as a “cheap” alternative to custom services. Compared to a custom service, the Data Entity can act as the data contract and removes the need of querying/saving a complicated table structure.

There are two kind of actions you can add, collection level (static) actions and instance level actions. You can find an example of each one of these two types in the data entity KRTRetailPeriodicDiscountEntity.

1.1    Collection level actions

A collection level method should be defined as static. The following is an example of such a method, it is used to implement complicated filtering, which would be otherwise not possible using oData queries, in order to return a subset of the data entity records.

    [SysODataActionAttribute("GetDiscountsOfChannel", false),
        SysODataCollectionAttribute("return", Types::Record, "KRTRetailPeriodicDiscountEntity")]
    public static List GetDiscountsOfChannel(RetailChannelId _retailChannelId, date _startBeforeDate)
    {
        KRTRetailPeriodicDiscountEntity discount;
        KRTRetailDiscountChannelEntity discountChannel;
        List discounts = new List(Types::Record);

        while
            select discount
            where   discount.ValidFrom <= _startBeforeDate
                &&  (   discount.ValidTo == dateNull()
                    ||  discount.ValidTo >= today())
            exists join discountChannel
            where discountChannel.OfferId == discount.OfferId
                && discountChannel.RetailChannelId == _retailChannelId
        {
            discounts.addEnd(discount);
        }

        return discounts;
    }


All oData actions should be decorated using the SysODataActionAttribute attribute and since this is a class level method the second parameter should be set to false. In this case, we want to return a list and in a similar way as for custom services we need to use another attribute (SysODataCollectionAttribute) to define the type of the list elements.

In order to test it through Fiddler we have to do a POST request. For actions, it is always a POST event if they are not updating the data entity. The structure of the request look like:

POST https://<base url>/data/<entity resource name>/Microsoft.Dynamics.DataEntities.<action name> HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access token>
Host: <base url>

{
    <JSON formatted method parameters>
}


And an example of calling the GetDiscountsOfChannel static method is

POST https://proddev0313cbbc0662fada44devaos.cloudax.dynamics.com/data/KRTRetailPeriodicDiscounts/
Microsoft.Dynamics.DataEntities.GetDiscountsOfChannel HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access token>
Host: proddev0313cbbc0662fada44devaos.cloudax.dynamics.com

{
    "_retailChannelId" : "000032",
    "_startBeforeDate" : "2017-06-26"
}


1.2    Instance actions

An instance actions, as you would expect, should be defined as a normal member method of the data entity i.e. not static. Calling an instance action requires that we first have a single data entity record to call it against, e.g. one retail discount. The following is an example of such a method, it is used to expose complicated business logic which finds all retail discounts overlapping with the selected retail discount.


    [SysODataActionAttribute("GetOverlappingDiscounts", true),
        SysODataCollectionAttribute("return", Types::String)]
    public List GetOverlappingDiscounts()
    {
        KRTRetailPeriodicDiscountEntity discount;
        List discounts = new List(Types::String);

        var discountOverlapCalculator = new RetailDiscountOverlapCalculator();
                   
discountOverlapCalculator.parmRetailPeriodicDiscount(
RetailPeriodicDiscount::findByOfferId(this.OfferId));

RetailDiscountAnalysisTmp overlappingDiscounts = 
discountOverlapCalculator.CalculateOverlapsForDiscount();

        while
            select discount
            exists join overlappingDiscounts
            where overlappingDiscounts.OfferId == discount.OfferId
        {
            discounts.addEnd(discount.OfferId);
        }
            
        return discounts;
    }


Again, we should decorate the method with the SysODataActionAttribute attribute. However now the second parameter should be set to true.

Testing it through fiddler requires that we combine two oData features, reading a record by primary key and calling an action for it. Querying based on the primary key of the record is done using the following request format

GET https://<base URL>/data//<entity resource name>(<keyField1> = <value1>,<keyfield2> = <value2> …) HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access token>
Host: <base URL>


An example of fetching a KRTRetailPeriodicDiscountEntity record by primary key is

GET https://proddev0313cbbc0662fada44devaos.cloudax.dynamics.com/data/KRTRetailPeriodicDiscounts(
OfferId='ST100002',dataAreaId='USRT')  HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access token>
Host: proddev0313cbbc0662fada44devaos.cloudax.dynamics.com


We will have to change somewhat the basic primary key oData query to call the actionl. Action calls should be done using POST requests. We also need to specify the method and any parameters. The structure of the request look like:

POST https://<base URL>/data//<entity resource name>(<keyField1> = <value1>,<keyfield2> = <value2> …)/Microsoft.Dynamics.DataEntities.<action name> HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access token>
Host: <base URL>

{
    <JSON formatted method parameters>
}


And an example of calling the GetOverlappingDiscounts action is

POST https://proddev0313cbbc0662fada44devaos.cloudax.dynamics.com/data/
KRTRetailPeriodicDiscounts(OfferId='ST100002',dataAreaId='USRT')/
Microsoft.Dynamics.DataEntities.GetOverlappingDiscounts HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer <access token>
Host: proddev0313cbbc0662fada44devaos.cloudax.dynamics.com

{
}


2    Troubleshooting

oData responses can sometimes be very misleading. The following is a list of errors and the corresponding oData behavior.

Failing to define a collection action as static or defining an instance action as static will have the effect that AX compiler will silently ignore the action and not include it in the oData metadata.

When querying using a primary key if you do not specify all key fields correctly (e.g. omit dataAreaId), oData will respond with the not so helpful message "No HTTP resource was found that matches the request URI”.

When calling oData actions if you don’t specify the correct number of parameters, oData will respond with an exception mentioning "InvokeAction - Model state is invalid."

If you try to define an optional parameter in the AX method that implements the oData action, when calling it through oData you will get a message mentioning "Ambiguous match found."

Search

About

DaxOnline.org is free platform that allows you to quickly store and reuse snippets, notes, articles related to Dynamics AX.

Authors are allowed to set their own AdSense units.
Join us.

Blog Tags