Adding/Creating/Deleting a record using an ODATA web service

ClermensLinders

New member
Joined
Mar 12, 2020
Messages
2
Programming Experience
10+
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.
C#:
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.
C#:
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;
                }

                field("Last name"; "Last name")
                {
                    ApplicationArea = Basic;
                }

                field(FunctionName; FunctionName)
                {
                    ApplicationArea = Basic;

                }
            }
        }
    }
}

Than there is a list page, that is used in D365BC to get a list of records, not really needed for the web service, but I provide it just to be complete.
C#:
page 50109 "Workers List"
{
    PageType = List;
    ApplicationArea = All;
    UsageCategory = Lists;
    SourceTable = Workers;

    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field("No."; "No.")
                {
                    ApplicationArea = Basic;
                }

                field("First name"; "First name")
                {
                    ApplicationArea = Basic;
                }

                field("Last Name"; "Last Name")
                {
                    ApplicationArea = Basic;
                }

                field(FunctionName; FunctionName)
                {
                    ApplicationArea = Basic;
                }
            }
        }
    }
}

In order to be able to create the web service we need to provide a little xml file to D365BC. I provide this, just to be complete.
XML:
<?xml version = "1.0" encoding = "utf-8" ?>
<ExportedData>
    <TenantWebServiceCollection>
        <TenanatWebService>
            <ObjectType>Page</ObjectType>
            <ObjectID>50108</ObjectID>
            <ServiceName>WorkersWebService</ServiceName>
            <Published>true</Published>
        </TenanatWebService>
    </TenantWebServiceCollection>
</ExportedData>

Using C# to read from the ODATA web service is no problem:
C#:
listBox1.Items.Clear();
WorkersReadFromAlWebService = new List<WorkerClass>();
string _wsURL = "https://api.businesscentral.dynamics.com/v2.0/SomeFunfyGuid/Sandbox/WS/CRONUS NL/Page/WorkersWS";
string _userName = "UserName";
string _wsKey = "Password";
//Create an instance of the D365BC SOAP WS
BasicHttpBinding _binding = new BasicHttpBinding();
//Set https usage
_binding.Security.Mode = BasicHttpSecurityMode.Transport;
_binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
using (WorkersWS_PortClient _ws = new WorkersWS_PortClient(_binding, new EndpointAddress(_wsURL)))
{
    _ws.ClientCredentials.UserName.UserName = _userName;
    _ws.ClientCredentials.UserName.Password = _wsKey;
    //Filter
    List<WorkersWS_Filter> _filters = new List<WorkersWS_Filter>();
    WorkersWS_Filter _filter = new WorkersWS_Filter
    {
        Field = WorkersWS_Fields.No,
        Criteria = "*"
    };
    
    _filters.Add(_filter);
    try
    {
        foreach (WorkersWS _workerWS in _ws.ReadMultiple(_filters.ToArray(), "", 0))
        {
            WorkerClass _wc = new WorkerClass();
            _wc.E_Tag = _workerWS.Key;
            _wc.First_Name = _workerWS.First_name;
            _wc.Last_Name = _workerWS.Last_Name;
            _wc.FunctionName = _workerWS.FunctionName;
            WorkersReadFromAlWebService.Add(_wc);
            listBox1.Items.Add(_workerWS.Last_Name + " / " + _workerWS.First_name + " (No: " + _workerWS.No + ")");
        }
    }
    catch (Exception _ex)
    {
        
    }
}

ClearWorker();//Clears some textboxes

My C# attempt to Update or Create a new record in the table using the ODATA web service:
C#:
string _url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS NL')/WorkersWS";
//string _url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV3/Company('CRONUS NL')/WorkersWS";//ODATA V3, makes no difference
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("UserNameassword"));
_request.Accept = "*/*";
_request.KeepAlive = true;
//_request.Method = "PUT";//error 405: Method is not allowed
_request.Method = "PATCH";//error 405: Methode is not allowed
string _etag = rtbE_Tag.Text;
_request.Headers["If-Match"] = String.Format("W/\"{0}\"", _etag);
//_request.Method = "POST";//(when using POST, the 4 lines above are commented out)error 400: invalid method
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)
{

}

So it makes no difference if I use Patch, Put or Post (except for the error number).
I tried to get an anser through D365BC forums, but I simply get no reply (at least not a reply that is of any help).
Finding answers yourselves is like the verbal 'needle in the haystack'. D365BC uses the relative new language AL. And if you find something it usually is for Navision, which uses C/AL, which is completely different.

I also tried a completely different approach by using the Nuget package manager and see what ODATA packages are there.
Many of these are created for MVC (I am using Win Forms).
But I tried:
- Microsoft.OData.Data (for use with ODATA v3, It doesn't simply install on VS 2019, but using something that I found online I got it to install on VS 2019, but all these sample assume an ODATA web service without authentication. Using authentication, requires setting some settings in the OdataClient.odata.config file. Well this file isn't created (perhaps only when using MVC), if I create it myself and I set the settings for authentication, it simply does nothing. I get an authentication error, password/username not set)
- Microsoft.OData.Core (for use with ODATA v4, behaves exactly like the .DATA version (so same problems), again no luck)
- Simple.Odata.Client (couldn't get authenticated, which is a show stopper)
- tried a third one as well (forgot the name), but also simply using the authentication is a problem.

When I speak about this problem with some people they more or less all claim that there isn't much difference between SOAP and ODATA. Yet SOAP works and ODATA doesn't.
I hope there is someone who has experience with this and is willing and capable to help me.

I will end with the SOAP code that works and which we can use to update a record:
C#:
string _wsURL = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/WS/CRONUS NL/Page/WorkersWS";
string _userName = "Username";
string _wsKey = "Password";
//Create an instance of the D365BC SOAP WS
BasicHttpBinding _binding = new BasicHttpBinding();
//Set https usage
_binding.Security.Mode = BasicHttpSecurityMode.Transport;
_binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
using (WorkersWS_PortClient _ws = new WorkersWS_PortClient(_binding, new EndpointAddress(_wsURL)))
{
    _ws.ClientCredentials.UserName.UserName = _userName;
    _ws.ClientCredentials.UserName.Password = _wsKey;
    try
    {
        WorkersWS _wws = _ws.Read(tbNo.Text);
        if (_wws.No == tbNo.Text)
        {
            _wws.First_name = tbFirstName.Text;
            _wws.Last_Name = tbLastName.Text;
            _wws.FunctionName = tbFunctionName.Text;
            _ws.Update(ref _wws);
            if (_wws.No == tbNo.Text)
            {
                tbResult.Text = "Update success.";
            }
        }
    }
    catch (Exception _ex)
    {
        
    }
}


Kind regards,


Clemens Linders

Kind regards,

Clemens Linders
 
Last edited:

JohnH

C# Forum Moderator
Staff member
Joined
Apr 23, 2011
Messages
906
Location
Norway
Programming Experience
10+
insertcode.png
 

Sheepings

Retired Programmer
Staff member
Joined
Sep 5, 2018
Messages
1,828
Location
UK
Programming Experience
10+
Many of these are created for MVC (I am using Win Forms).
Why would that be an issue?

I also agree, there is to much code above to cipher through. Perhaps you can break the problem down and narrow the codebase that you've presented so there is less code for us to dig through.
 
Top Bottom