Dynamics 365 Business Central: using OData V4 Bound Actions

I’ve promised this post to some attendees of my last Dynamics 365 Business Central development workshop in Microsoft Italy (c/o Microsoft House) last week.

Question was: How can I call Dynamics 365 Business Central logic from an external application? Simple answer given by all: you can publish a codeunit as web service and use the SOAP endpoint.

But if I want to use OData? You cannot publish a codeunit as an OData endpoint. Answer: you can call custom Dynamics 365 Business Central functions via ODataV4 by using Bound Actions. Bound Actions are actions related to an entity that you can call via an HTTP call and they can invoke your business logic (functions).

Unfortunately, documentation about how to use OData V4 bound actions with Dynamics 365 Business Central is quite poor and with this post I would like to help clearing this topic a bit more. There are two main scenarios that I want to cover here:

  • Calling a D365BC codeunit that performs business logic, like creating new entities
  • Calling a procedure in a D365BC codeunit by passing parameters and reading a return value

For this sample I’ve decided to create an extension with a codeunit that contains the business logic that I want to call via OData. My codeunit has two business functions:

  • CloneCustomer: it creates a new customer based on an existing customer record
  • GetSalesAmount: it gives the total sales amount for a given customer

The codeunit code is defined as follows:

ODataBoundActions_01

To use OData V4 bound actions you need to declare a function in a page and this function must have the [ServiceEnabled] attribute.

For this demo project, I would like to publish the Customer Card (page 21) as OData web service, so the natural thing to do is to create a pageextension object of the Customer Card to add our [ServiceEnabled] procedure and then publishing the Customer Card as web service. If you try to do this, it will never work!

If you declare a [ServiceEnabled] function in a pageextension object and you try to reach the metadata of the OData endpoint (baseurl/ODataV4/$metadata), you will not see the action published.

To publish your action attached to the Customer entity, you need to create a new page like the following and then publishing it as web service:

ODataBoundActions_02

Here, ODataKeyFields property specify what is the field to use as key when calling the OData endpoint (I want the “No.” field of the Customer record).

Inside this page, I declare two procedures to call the two methods defined above in our AL codeunit:

ODataBoundActions_03

Here:

  • CloneCustomer is a procedure called without parameters. It takes the context of the call and calls the CloneCustomer method defined in our codeunit.
  • GetSalesAmount is a procedure that takes a Code parameter, calls the GetSalesAmount procedure defined in our codeunit and returns the result as response.

What happens with the following definitions when we publish the MyCustomerCard page as web service (here called MyCustomerCardWS)?

If we reach the OData V4 metadata, we can see that now we have the actions published:

ODataBoundActions_04

Now we can try to call our bound actions via OData. As a first step, we want to call the CloneCustomer function. For this, we need to send a POST request to the following endpoint:

https://yourbaseurl/ODataV4/Company('CRONUS%20IT')/MyCustomerCardWS('10000')/NAV.CloneCustomer

I’m using the REST Client extension to send HTTP requests to the above endpoint. This is the request sent:

ODataBoundActions_05

and this is the result of this call:

ODataBoundActions_06

What happens on Dynamics 365 Business Central? The code in our codeunit is called and we have a Customer record created (cloned by the customer with “No.” = 10000 as the input):

ODataBoundActions_07.jpg

Our second function to call (GetSalesAmount) wants a Code[20] parameter as input (not needed but it’s only to show hot to pass parameters to a bound action). We need to send a POST request to the following endpoint:

https://yourbaseurl/ODataV4/Company('CRONUS%20IT')/MyCustomerCardWS('10000')/NAV.GetSalesAmount

by passing a JSON body with the parameters (name and value).

This is the request sent:

ODataBoundActions_08

and this is the response:

ODataBoundActions_09

We have the value of the total sales amount for the given customer (retrieved by calling our codeunit method).

Here there’s a point to remember, because it was for me a source of hours spent on debugging: parameter’s name to pass on the JSON object must match with the OData metadata, not with your function’s parameters.

For example, if you declare the bound action as follows:

ODataBoundActions_10

where CustomerNo parameter has capital letters, the OData metadata is as follows:

ODataBoundActions_11

so the JSON must be passed accordingly (parameters names must match).

Not so easy, but very powerful when you understand the logic 🙂

 

 

38 Comments

  1. Hi Stefano,

    I Hope you can help me.
    I’m struggling with calls to my OData custom procedures. I succeed in publish my procedure as a Bound Action with [ServiceEnabled] annotation, but when I try to call it, it does nothing (return a 200 HTTP Status code regardless I exit my procedure with actionContext.SetResultCode(WebServiceActionResultCode::Updated)) when I declare a void procedure like
    [ServiceEnabled]
    procedure CloneCustomer()
    or replies with an error like:
    “error”: {
    “code”: “Unknown”,
    “message”: ” Object reference not set to an instance of an object. CorrelationId: cd0b2024-b486-4b19-bb44-35f0dde76f4d.”
    }
    If I declare a procedure with a parameter like :
    [ServiceEnabled]
    procedure CloneCustomer(actionContext: WebServiceActionContext)

    Any suggestions? Thanks!

    Like

  2. Hellow again,

    I follow your post, and you are right, it works as expected. So, I check every single step and I finally find what was going on with error “Object reference not set to an instance of an object”, and it is because I wrote my Bound Action without the var keyword before parameter WebServiceActionContext. So I’ve changed from:
    procedure CloneCustomer(actionContext: WebServiceActionContext)
    To:
    procedure CloneCustomer(var actionContext: WebServiceActionContext)

    And error was solved! My bad.

    So, let me ask you another question, how can I debug or log to see why my code is not working as expected?
    Thanks again Stefano.!

    Like

  3. I’m struggeling with authentication issues. I always get 401 Unauthorized returned.
    How do you need to build the http header if you have username = myUser and password = Test1234 ??
    Using a browser, I always can authenticate using username & password. NTLM is enabled on BC Service.

    Like

    1. If you’re using D365BC onpremise with Windows Authentication you need to use username (Domain\Username) and the domani password (not web service access key)

      Like

      1. If you’re using Windows Authentication in D365BC and you’re using ODATA with basic authentication it works (I’ve different applications that uses this type of authentication). With C# is new NetworkCredential(user,pwd).

        Like

      2. I’m trying to create a HTTP Request in Postman.

        Content-Type:application/x-www-form-urlencoded
        Authorization: Basic myUser Test1234

        will return 401 Unauthorized,

        Like

  4. Felix did you fix the issue? I’m still facing same 401 via client but Im able to access same URLs in browser.

    Like

    1. Hi Renno,

      I’m developing in c# and this code works for me, hope you can use it:

      WebRequest webRequest = WebRequest.Create(uri);
      webRequest.ContentType = “application/json”;
      webRequest.Method = “GET”;

      NetworkCredential currentNetworkCredential = new NetworkCredential()
      {
      Domain = DOMAIN,
      UserName = USERNAME,
      Password = PASSWORD
      };

      CredentialCache credentialsCache = new CredentialCache
      {
      {uri, “NTLM”, currentNetworkCredential}
      };

      webRequest.Credentials = credentialsCache;

      Like

  5. Hi Stefano !

    Thanks for useful tips !

    Tried to use your brilliant tip for an API instead of a page odata web service, but cannot find the exact syntax for the URL …

    I have an API function for the Customer Discount Group table, and added a simple function to this table , named getSomething having in parameter text ( called it customerNo) and returns a text …
    Just to test – the idea is a function to call by parameter, and return an answer ..

    I have added the function like you described for your function GetSalesAmount with ServiceEnabled,
    And I find the service in the Edmx service description file …

    When I call the API (function custDiscountGroups) using POSTMAN then it works :

    https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/custDiscountGroups

    {
    “@odata.context”: “https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/$metadata#companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/custDiscountGroups”,
    “value”: [
    {
    “@odata.etag”: “W/\”JzQ0O1JUY0I4WkVpdVhQYXFUd29sRDM0VXJ4aFgwSS9qYlFtRXpwM1NvYzRHSmM9MTswMDsn\””,
    “custDiscountGroup”: “DETALJ”,
    “description”: “Detalj”
    },
    {
    “@odata.etag”: “W/\”JzQ0O2Myb3pKdXhKL2d4RU1NcnVrOVRMbXJOS0JQdHE5cFFNTG9ZSnloVTY0OE09MTswMDsn\””,
    “custDiscountGroup”: “STORKUNDE”,
    “description”: “Storkunder”
    }
    ]
    }
    ************************************************

    But how do I call the getSomething function – have tried a lot of different syntax :

    NEED HELP FOR CORRECT SYNTAX !!

    *************************************************
    https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/custDiscountGroups/Microsoft.NAV.getSomething

    https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/custDiscountGroups/NAV.getSomething

    https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/Microsoft.NAV.getSomething

    https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/custDiscountGroups/getSomething

    https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/NAV.getSomething

    {
    “error”: {
    “code”: “BadRequest_NotFound”,
    “message”: “The request URI is not valid. Since the segment ‘custDiscountGroups’ refers to a collection,
    this must be the last segment in the request URI or it must be followed by an function or action that can be bound to it otherwise all intermediate segments must refer to a single resource.
    CorrelationId: 6f3a6627-2fd1-4bdd-b09e-635fdbf71107.”
    }
    }

    Content from Edmx refers to namespace Microsoft.NAV , and the getSomething function is defined in there ..


    +

    Like

      1. edmx:DataServices
        Schema Namespace=”Microsoft.NAV” xmlns=”http://docs.oasis-open.org/odata/ns/edm”
        :
        :
        :
        EntityType Name=”custDiscountGroup”
        Key
        PropertyRef Name=”custDiscountGroup”
        Key
        Property Name=”custDiscountGroup” Type=”Edm.String” Nullable=”false” MaxLength=”20″
        Property Name=”description” Type=”Edm.String” MaxLength=”100″
        EntityType
        Action Name=”getSomething” IsBound=”true”
        Parameter Name=”bindingParameter” Type=”Microsoft.NAV.custDiscountGroup”
        Parameter Name=”customerNo” Type=”Edm.String”
        ReturnType Type=”Edm.String”
        Action

        Like

  6. Hi Demiliani. I’m presently doing one third party integration with Business Central on cloud edition and followed your steps for odata v4 bound actions… all the steps i followed but my customized actions are not showing in MS Azure logic app but there it shows the standard all actions as methos. May i know what could be the issue ?…

    Like

      1. Hi Demiliani Thanks for your quick response, Really Appreciable.

        that means in the connector only standard action only will show, it won’t show my customized method as a action there..is that you mean

        Like

  7. Hi Stefano,

    I created a codeunit to generate Json data from Sales Quote to third party system endpoint. The same way, I created another codeunit to which the third party needs to Post the data. Is it wrong to create codeunit & publish webservices for third parties to post json ? Do I need to follow Odata Bound actions in this case? Is there anything other methods to achieve the same?

    Like

  8. Hi Stefano,

    I follow the steps and managed to create bound actions to release and reopen sales orders (thank you)

    I’m working on Business Central 15.5 On-premise

    I tried to create another “simple” bound action to print a sales order confirmation, but I can’t make it work.

    [ServiceEnabled]
    procedure Print(var actionContext: WebServiceActionContext)
    var
    DocPrint: Codeunit “Document-Print”;
    Usage: Option “Order Confirmation”,”Work Order”,”Pick Instruction”;
    begin
    DocPrint.PrintSalesOrder(Rec, Usage::”Order Confirmation”);

    actionContext.SetObjectType(ObjectType::Page);
    actionContext.SetObjectId(Page::SalesOrderList);
    actionContext.AddEntityKey(Rec.FieldNo(“Document Type”), Rec.”Document Type”);
    actionContext.AddEntityKey(Rec.FieldNo(“No.”), Rec.”No.”);

    actionContext.SetResultCode(WebServiceActionResultCode::Updated);
    end;

    I test the web service with PostMan
    {baseurl}/ODataV4/Company(‘CRONUS%20France%20S.A.’)/ListeCdeMultiSoc(‘Order’,’1001′)/NAV.Print

    The return status is OK, but there is no printing

    There is a warning on the Business Central server, in the Windows Event Viewer : Cannot download print stream to client for report ID 1305 unsupported client type WebServiceClient.

    Is it not possible to create a bound action to print documents ?

    Like

  9. Hi Stefano, thanks a lot for your tips. Right now i’m trying to create unbound procedure for my project which use odatav4.
    Do you have any simple example on creating unbound one ?

    Like

    1. Unbound actions are easy. Just publish a codeunit as ODataV4 endpoint and call the following endpoint:

      POST /ODataV4/{serviceName}_{procedureName}?company={companyName}
      and in the body of the request, pass a JSON with the procedure parameters.

      Like

  10. Hi Demiliani,
    for clone i have no issue.
    but for get return value
    I got this error message :
    {“error”:{“code”:”Unknown”,”message”:”Object reference not set to an instance of an object. CorrelationId: 772b0535-1084-4dc2-911e-7dee156bd7ff.”}}

    I tested using Postmant using POST method
    https://api.businesscentral.dynamics.com/v2.0/tenantname/Sandbox/ODataV4/Company('Cronus%20Demo%20Singapore%20Pte%20Ltd‘)/WTSSalesInvoices(‘IN20-00114’)/NAV.GetExtDocNo

    #1 My Codeunit
    procedure GetExtDocNo(ParDocNo: Code[20]): Text
    var
    SalesHeader: Record “Sales Header”;
    begin
    if SalesHeader.get(SalesHeader.”Document Type”::Invoice, ParDocNo) then
    exit(SalesHeader.”External Document No.”)
    else
    exit(‘Not Found’);
    end;

    #2 My New Page
    [ServiceEnabled]
    procedure GetExtDocNo(ParDocNo: Code[20]): Text
    var
    WSCodeunit: Codeunit “WS Codeunit”;
    RetValue: Text;
    actionContext: WebServiceActionContext;
    begin
    actionContext.SetObjectType(ObjectType::Page);
    actionContext.SetObjectId(Page::”WTS Sales Invoices”);
    actionContext.AddEntityKey(Rec.FieldNo(“No.”), Rec.”No.”);
    RetValue := ‘WS – ‘ + WSCodeunit.GetExtDocNo(Rec.”No.”);
    actionContext.SetResultCode(WebServiceActionResultCode::Get);
    exit(RetValue);
    end;

    Like

  11. Hi Demiliani,
    What about complex json
    {
    “customers”:[
    { “firstName” : “John”,
    “lastName” : “Doe”}
    ]
    }

    do you know how do I do the procedure?

    [ServiceEnabled]
    procedure test(customers: Text; firstName: Text; lastName: Text): Text
    begin
    exit(‘endTest’)
    end

    Like

  12. Hi Demiliani ,
    Thank you a lot for this Post ,
    I’m aiming to use this concept of Bound Actions via Odata4 , the only limitation is that we are using only primitive Types on the actions like for example the parameter of the function getsalesamount is a String that we will use on the Request Body.

    is it Possible to use a complete JsonObject with collections on the Bounded Actions ?
    The purpose is to passe through the functions a complete JsonObject that we will parse after like the API Pages with Header/Lines For example?

    Thank you

    Like

  13. hi Stefano, if I have a codeunit exposed as SOAP, with different methods exposed, which have an xmlport as input, how can I transform this into BoundAction?
    es:
    Codeunit 50000 “ExposedFunctions”
    {
    procedure SendVendors (UpdateVendors: XmlPort UpdateVendors): Text
    begin
    UpdateVendors.Import();
    …….
    end;
    }

    Like

  14. Does NTLM realy works with bound / unbound actions?
    User / Password works without any problem.

    NTLM with Bound actions gives me:
    Status 500Object reference not set to an instance of an object.
    Unbound: 500Index was out of range

    Did u tested it?
    Do you have any ideas?

    Best Regards,
    Michael

    Like

    1. Not tested with NTLM but it should work (it’s cross authentication type, it’s an Odata feature). I can assure that it works with OAuth for example.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.