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:
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:
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:
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:
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:
and this is the result of this call:
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):
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:
and this is the response:
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:
where CustomerNo parameter has capital letters, the OData metadata is as follows:
so the JSON must be passed accordingly (parameters names must match).
Not so easy, but very powerful when you understand the logic 🙂
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!
LikeLike
Can you see your published function in OData $metadata endpoint? I think that if you follow the post you should be able to do so.
LikeLike
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.!
LikeLike
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.
LikeLike
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)
LikeLike
It’s not working. HTTP Documentation tells something about Base64 encoding
LikeLike
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).
LikeLike
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,
LikeLike
Felix,
Postman has the NTLM authorization feature (i think in beta but it works) and you has to choose it, not Basic.
LikeLike
Felix did you fix the issue? I’m still facing same 401 via client but Im able to access same URLs in browser.
LikeLike
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;
LikeLike
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 ..
–
–
–
+
–
–
–
–
LikeLike
The EDMX file was removed …
LikeLike
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
LikeLike
:
:
:
LikeLike
Is it possible to return an xml or json structure?
LikeLike
You can have methods that returns a Text variable or directly an XmlDocument or a JsonObject
LikeLike
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 ?…
LikeLike
In Logic Apps an unbound action must be called by using as a normal OData endpoint (http action). The standard D365BC connector will not show it.
LikeLike
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
LikeLike
If i use Logic App Custom Connector…through that how will i call my method from codeunit?
LikeLike