C# and ODATA CRUD communication with D365BC web service
-
I have a problem where C# cannot Create/Update/Delete through an ODATA web service running from D365BC. PS I am using VS 2019, C# winforms. Our company is going to use Dynamics 365 Business Central (D365BC), which uses the programming language AL. AL is a nice language but some things we will simply prefer to do in C#. In D365BC you can easily create tables and pages (objects that consume these tables). And for each page you can create a web service by simply clicking a checkbox. These web service can be used to communicate via SOAP/ODATA v3 and ODATA v4. All are available at the same time. So creating a web service that allows communication with a table created in D365BC is fairly easy. If I use the SOAP webservice I can perform all CRUD functions. But when I use ODATA (either v3 or v4) I can read from the web service but I cannot Create/Update or Delete. The reason that we may occasionally want to use ODATA, is that ODATA is faster than SOAP. The code I have in D365BC: A simple table where every field is a record in a SQL table.
table 50109 "Workers"
{
DataClassification = ToBeClassified;fields { field(1; "No."; Code\[20\]) { DataClassification = ToBeClassified; } field(10; "First name"; Text\[50\]) { DataClassification = ToBeClassified; } field(20; "Last Name"; Text\[50\]) { DataClassification = ToBeClassified; } field(40; FunctionName; Text\[50\]) { DataClassification = ToBeClassified; } } trigger OnInsert() var myInt: Integer; begin end; trigger OnModify() var myInt: Integer; begin end; trigger OnDelete() var myInt: Integer; begin end;
}
The card page that uses the table and has the ability to make a web service out of the used table.
page 50108 "Workers Card"
{
PageType = Card;
ApplicationArea = All;
UsageCategory = Administration;
SourceTable = Workers;layout { area(Content) { group(General) { field("No."; "No.") { ApplicationArea = Basic; Importance = Promoted; } field("First name"; "First name") { ApplicationArea = Basic; }
-
I have a problem where C# cannot Create/Update/Delete through an ODATA web service running from D365BC. PS I am using VS 2019, C# winforms. Our company is going to use Dynamics 365 Business Central (D365BC), which uses the programming language AL. AL is a nice language but some things we will simply prefer to do in C#. In D365BC you can easily create tables and pages (objects that consume these tables). And for each page you can create a web service by simply clicking a checkbox. These web service can be used to communicate via SOAP/ODATA v3 and ODATA v4. All are available at the same time. So creating a web service that allows communication with a table created in D365BC is fairly easy. If I use the SOAP webservice I can perform all CRUD functions. But when I use ODATA (either v3 or v4) I can read from the web service but I cannot Create/Update or Delete. The reason that we may occasionally want to use ODATA, is that ODATA is faster than SOAP. The code I have in D365BC: A simple table where every field is a record in a SQL table.
table 50109 "Workers"
{
DataClassification = ToBeClassified;fields { field(1; "No."; Code\[20\]) { DataClassification = ToBeClassified; } field(10; "First name"; Text\[50\]) { DataClassification = ToBeClassified; } field(20; "Last Name"; Text\[50\]) { DataClassification = ToBeClassified; } field(40; FunctionName; Text\[50\]) { DataClassification = ToBeClassified; } } trigger OnInsert() var myInt: Integer; begin end; trigger OnModify() var myInt: Integer; begin end; trigger OnDelete() var myInt: Integer; begin end;
}
The card page that uses the table and has the ability to make a web service out of the used table.
page 50108 "Workers Card"
{
PageType = Card;
ApplicationArea = All;
UsageCategory = Administration;
SourceTable = Workers;layout { area(Content) { group(General) { field("No."; "No.") { ApplicationArea = Basic; Importance = Promoted; } field("First name"; "First name") { ApplicationArea = Basic; }
Rather than building the request body by hand, you should use a JSON serializer to create it. Add a NuGet package reference to JSON.NET[^], add a
using
directive for theNewtonsoft.Json
namespace, and change your code to:var body = new
{
No = tbNo.Text,
First_name = tbFirstName.Text,
Last_Name = tbLastName.Text,
FunctionName = tbFunctionName.Text,
};string json = JsonConvert.SerializeObject(body);
byte[] data = Encoding.UTF8.GetBytes(json);NB: You'll want to use the UTF8 encoding rather than ASCII, since that's what you've declared in your content-type header. I'd also be inclined to use the
HttpClient
class[^] rather than the low-levelHttpWebRequest
class: Call a Web API From a .NET Client (C#) - ASP.NET 4.x | Microsoft Docs[^]
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
-
Rather than building the request body by hand, you should use a JSON serializer to create it. Add a NuGet package reference to JSON.NET[^], add a
using
directive for theNewtonsoft.Json
namespace, and change your code to:var body = new
{
No = tbNo.Text,
First_name = tbFirstName.Text,
Last_Name = tbLastName.Text,
FunctionName = tbFunctionName.Text,
};string json = JsonConvert.SerializeObject(body);
byte[] data = Encoding.UTF8.GetBytes(json);NB: You'll want to use the UTF8 encoding rather than ASCII, since that's what you've declared in your content-type header. I'd also be inclined to use the
HttpClient
class[^] rather than the low-levelHttpWebRequest
class: Call a Web API From a .NET Client (C#) - ASP.NET 4.x | Microsoft Docs[^]
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
Hi Richard, Thanks for your reply. If I used the code you profided me but I cannot pass 'data' to _client as it expects a class and not Json data. If I create a class and fill it, like in the MS Docs page you provided, than I get error 401 not authorized.
WorkersClass \_data = new WorkersClass(); \_data.E\_Tag = string.Empty; \_data.No = tbNo.Text; \_data.First\_name = tbFirstName.Text; \_data.Last\_Name = tbLastName.Text; \_data.FunctionName = tbFunctionName.Text; var url = await CreateWorkerAsync(\_data); static async Task CreateWorkerAsync(WorkersClass \_worker) { string \_url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService";//Card Page string \_userName = "UserName"; string \_wsKey = "Password"; \_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\_userName, \_wsKey); HttpResponseMessage response = await \_client.PostAsJsonAsync(\_url, \_worker); response.EnsureSuccessStatusCode(); // return URI of the created resource. return response.Headers.Location; }
I tried the following:
var body = new { E\_Tag = string.Empty, // Unique key No = tbNo.Text, First\_name = tbFirstName.Text, Last\_Name = tbLastName.Text, FunctionName = tbFunctionName.Text, }; string json = JsonConvert.SerializeObject(body); byte\[\] data = Encoding.UTF8.GetBytes(json); var url = await CreateWorkerAsync(data);
The error I get is: Argument 1: cannot convert from byte[] to WorkersClass. If I look at _client.PostAsJsonAsync, than I don't see a other solution where it says I can pass Json data? I feel that I am close, but I think I miss the final step. Hope you can help me. Kind regards, Clemens Linders
-
Hi Richard, Thanks for your reply. If I used the code you profided me but I cannot pass 'data' to _client as it expects a class and not Json data. If I create a class and fill it, like in the MS Docs page you provided, than I get error 401 not authorized.
WorkersClass \_data = new WorkersClass(); \_data.E\_Tag = string.Empty; \_data.No = tbNo.Text; \_data.First\_name = tbFirstName.Text; \_data.Last\_Name = tbLastName.Text; \_data.FunctionName = tbFunctionName.Text; var url = await CreateWorkerAsync(\_data); static async Task CreateWorkerAsync(WorkersClass \_worker) { string \_url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService";//Card Page string \_userName = "UserName"; string \_wsKey = "Password"; \_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\_userName, \_wsKey); HttpResponseMessage response = await \_client.PostAsJsonAsync(\_url, \_worker); response.EnsureSuccessStatusCode(); // return URI of the created resource. return response.Headers.Location; }
I tried the following:
var body = new { E\_Tag = string.Empty, // Unique key No = tbNo.Text, First\_name = tbFirstName.Text, Last\_Name = tbLastName.Text, FunctionName = tbFunctionName.Text, }; string json = JsonConvert.SerializeObject(body); byte\[\] data = Encoding.UTF8.GetBytes(json); var url = await CreateWorkerAsync(data);
The error I get is: Argument 1: cannot convert from byte[] to WorkersClass. If I look at _client.PostAsJsonAsync, than I don't see a other solution where it says I can pass Json data? I feel that I am close, but I think I miss the final step. Hope you can help me. Kind regards, Clemens Linders
I was referring to the code from your original post, where you're constructing a JSON string manually:
Quote:
string body = "{" + Environment.NewLine +
"\"No\":" + tbNo.Text + "," + Environment.NewLine +
"\"First_name\":\"" + tbFirstName.Text + "\"," + Environment.NewLine +
"\"Last_Name\":\"" + tbLastName.Text + "\"," + Environment.NewLine +
"\"FunctionName\":\"" + tbFunctionName.Text + "\"," + Environment.NewLine +
"}";byte[] data = Encoding.ASCII.GetBytes(body);
try
{
_request.ContentLength = data.Length;Stream requestStream = \_request.GetRequestStream(); requestStream.Write(data, 0, data.Length); requestStream.Close(); HttpWebResponse \_response = \_request.GetResponse() as HttpWebResponse;//Here we get the exception errors 400 or 405 Console.WriteLine(\_response.StatusCode);
}
catch (Exception ex)
{}
You don't need to worry about the JSON serialization with the
HttpClient
and thePostAsJsonAsync
method - it handles the serialization for you. You're getting a 401 error with your new code because you're passing the wrong values to theAuthenticationHeaderValue
. The first parameter is the scheme, which should be"BASIC"
, and the second is the parameter, which should be the combined username and password. AuthenticationHeaderValue Constructor (System.Net.Http.Headers) | Microsoft Docs[^]string _userName = "UserName";
string _wsKey = "Password";
byte[] authenticationParameter = Encoding.UTF8.GetBytes(_userName + ":" + _wsKey);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authenticationParameter));
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
-
I was referring to the code from your original post, where you're constructing a JSON string manually:
Quote:
string body = "{" + Environment.NewLine +
"\"No\":" + tbNo.Text + "," + Environment.NewLine +
"\"First_name\":\"" + tbFirstName.Text + "\"," + Environment.NewLine +
"\"Last_Name\":\"" + tbLastName.Text + "\"," + Environment.NewLine +
"\"FunctionName\":\"" + tbFunctionName.Text + "\"," + Environment.NewLine +
"}";byte[] data = Encoding.ASCII.GetBytes(body);
try
{
_request.ContentLength = data.Length;Stream requestStream = \_request.GetRequestStream(); requestStream.Write(data, 0, data.Length); requestStream.Close(); HttpWebResponse \_response = \_request.GetResponse() as HttpWebResponse;//Here we get the exception errors 400 or 405 Console.WriteLine(\_response.StatusCode);
}
catch (Exception ex)
{}
You don't need to worry about the JSON serialization with the
HttpClient
and thePostAsJsonAsync
method - it handles the serialization for you. You're getting a 401 error with your new code because you're passing the wrong values to theAuthenticationHeaderValue
. The first parameter is the scheme, which should be"BASIC"
, and the second is the parameter, which should be the combined username and password. AuthenticationHeaderValue Constructor (System.Net.Http.Headers) | Microsoft Docs[^]string _userName = "UserName";
string _wsKey = "Password";
byte[] authenticationParameter = Encoding.UTF8.GetBytes(_userName + ":" + _wsKey);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authenticationParameter));
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
Hi Richard, I changed the CreateWorkersAsync:
static async Task CreateWorkerAsync(WorkersClass \_worker) { string \_url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService";//Card Page string \_userName = "UserName"; string \_wsKey = "Password"; byte\[\] \_authenticationParameter = Encoding.UTF8.GetBytes(\_userName + ":" + \_wsKey); \_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(\_authenticationParameter)); HttpResponseMessage response = await \_client.PostAsJsonAsync(\_url, \_worker); response.EnsureSuccessStatusCode(); // return URI of the created resource. return response.Headers.Location; }
I didn't make any other changes. I now no longer get the error 401 Not Authorized. Now I get the error: 400 Bad request We're one step closer. Do you have any idea why I could get this Bad request?? Kind regards, Clemens Linders
-
Hi Richard, I changed the CreateWorkersAsync:
static async Task CreateWorkerAsync(WorkersClass \_worker) { string \_url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService";//Card Page string \_userName = "UserName"; string \_wsKey = "Password"; byte\[\] \_authenticationParameter = Encoding.UTF8.GetBytes(\_userName + ":" + \_wsKey); \_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(\_authenticationParameter)); HttpResponseMessage response = await \_client.PostAsJsonAsync(\_url, \_worker); response.EnsureSuccessStatusCode(); // return URI of the created resource. return response.Headers.Location; }
I didn't make any other changes. I now no longer get the error 401 Not Authorized. Now I get the error: 400 Bad request We're one step closer. Do you have any idea why I could get this Bad request?? Kind regards, Clemens Linders
A 400 error would suggest that the server doesn't recognise the request format. Are you certain they accept JSON requests? Your original post mentioned a SOAP request, which is a dialect of XML. If they only support SOAP requests, you won't be able to post JSON data to the service.
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
-
A 400 error would suggest that the server doesn't recognise the request format. Are you certain they accept JSON requests? Your original post mentioned a SOAP request, which is a dialect of XML. If they only support SOAP requests, you won't be able to post JSON data to the service.
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
Hi Richard, I am pretty sure that Json shouldn't be a problem, because when I read the data I use a Json deserializer. There really are three different kind of web services: - SOAP - ODATA v3 - ODATA v4 This is the working code I use to read data using the ODATA V4 web service:
listBox1.Items.Clear(); WorkersReadFromAlWebService = new List(); string \_url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService";//CardPage HttpWebRequest \_request = (HttpWebRequest)WebRequest.Create(\_url); \_request.ContentType = "application/json; charset=utf-8"; \_request.Headers\["Authorization"\] = "Basic " + Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes("UserName:Password")); \_request.PreAuthenticate = true; HttpWebResponse \_response = \_request.GetResponse() as HttpWebResponse; using (Stream \_responseStream = \_response.GetResponseStream()) { StreamReader \_reader = new StreamReader(\_responseStream, Encoding.UTF8); string \_content = \_reader.ReadToEnd(); string \_jasonPart = GetJsonPartMultiRecord(\_content); List \_jWorkers = JsonConvert.DeserializeObject\>(\_jasonPart); foreach (var \_worker in \_jWorkers) { WorkersReadFromAlWebService.Add(\_worker); listBox1.Items.Add(\_worker.Last\_Name + " / " + \_worker.First\_name + " (No: " + \_worker.No + ")"); } } ClearWorker();