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
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?
LikeLike
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 ?
LikeLike
Unfortunately it’s not so easy. Printing on cloud is different. You cannot directly print to a local printer because the cloud cannot see your printers.
LikeLike
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 ?
LikeLike
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.
LikeLike
Hi Demiliani, thanks for quick response. I tried calling the proedure via postman but the “request data invalid” error showed up.
TenantID: 28sample
Environment: Sandbox
Codeunit name : TestCodeunit
Procedure name: CreateItem
Procedure input: No.
Company name: Cronus
request type:
post
url:
https://api.businesscentral.dynamics.com/ODataV4/TestCodeunit_CreateItem?company=Cronus
body:
{
“No”: “1000”
}
what goes wrong on this ?
LikeLike
I think the url is not correct. It should be something like:
https://api.businesscentral.dynamics.com/v2.0/TENANTID/Sandbox/ODataV4/TestCodeunit_CreateItem?company=CRONUS
LikeLike
Hi Demiliani, superb ! Thanks a lot for your quick response !
LikeLike
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;
LikeLike
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
LikeLike
Complex parameters are not supported. You can accept a Text parameter that contains your JSON if this suits your needs.
LikeLike
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
LikeLike
This is not supported as parameter type unfortunately.
LikeLike
Thanks a lot 🙂
LikeLike
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;
}
LikeLike
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
LikeLike
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.
LikeLike