simple-odata-client / Simple.OData.Client

MIT License
329 stars 191 forks source link

trying to communicate from C# with D365BC Odata v4 web service #734

Closed clemenslinders closed 4 years ago

clemenslinders commented 4 years ago

LS,

I am trying to communicate with a D365BC web service. If I use HttpClient there is no problem.

But if I use Simple.Odata.Client v4 than I get an error: Not found: {"error":{"code":"BadRequest_NotFound","message":"The request URI is not valid. Since the segment 'WorkersWebService' 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.

I installed the following packages: //Added Nuget: Install-Package Newtonsoft.Json -Version 12.0.3 //Added Nuget: Install-Package Microsoft.AspNet.WebApi.Client -Version 5.2.7 //Added Nuget: Install-Package Simple.OData.V4.Client -Version 5.12.0

I created a global variable:

    ODataClient Client = new ODataClient("https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService");

As no credentials are set I included at OnLoad:

        ODataClientSettings settings = new ODataClientSettings()
        {
            BaseUri = new Uri("">api.businesscentral.dynamics.com/.../WorkersWebService"),
            Credentials = new NetworkCredential("UserName", "Password")
        };
        Client = new ODataClient(settings);

And as Button.click I included:

    private async void button1_Click(object sender, EventArgs e)
    {
        try
        {
            var workers = await Client.For<Worker>().FindEntriesAsync();
        }
        catch (Exception ex)
        {

        }
    }

The error occurs at this line: var workers = await Client.For().FindEntriesAsync();

I think the problem lies in authentication.

The code I use with HttpClient is: private void btnFindWorkerOdata_Click(object sender, EventArgs e) {//WORKS OK listBox1.Items.Clear(); WorkersReadFromAlWebService = new List(); string _url = "https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/Company('CRONUS%20NL')/WorkersWebService"; 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()) { var x = _response.Headers.AllKeys; StreamReader _reader = new StreamReader(_responseStream, Encoding.UTF8); string _content = _reader.ReadToEnd(); string _jasonPart = GetJsonPartMultiRecord(_content); List _jWorkers = JsonConvert.DeserializeObject<List>(_jasonPart); foreach (var _worker in _jWorkers) { WorkersReadFromAlWebService.Add(_worker); listBox1.Items.Add(_worker.Last_Name + " / " + _worker.First_name + " (No: " + _worker.No + ")"); } } ClearWorker(); }

This HttpClinet code works, but inserting a record is my real problem so that is why I want to try Simple.Odata.Client. But before I can do that I need to be able to communicate with the web service

I hope someone can help me.

Kind regards,

Clemens Linders

wintg-zns commented 4 years ago

@clemenslinders Sorry this is a little late. Perhaps this will help someone.

We are using Simple.OData.Client to communicate with D365BC web services and APIs. When we create the ODataClientSettings object, we pass a base URL which is something like Web Services: https://api.businesscentral.dynamics.com/v2.0//sandbox/ODataV4/ APIs: https://api.businesscentral.dynamics.com/v2.0//sandbox/api////

We create the client from the Client Settings:

            ODataClientSettings clientSettings = new ODataClientSettings("https://api.businesscentral.dynamics.com/v2.0/<tenant_id>/sandbox/ODataV4/")
            {
                IncludeAnnotationsInResults = true,
                IgnoreResourceNotFoundException = true,
                IgnoreUnmappedProperties = true,
            };

            IODataClient client = new ODataClient(clientSettings);

When we make the OData call, we pass the name of the method (i.e. name of web service in BC):

IBoundClient<Worker> command = client.For("Company").Key("CRONUS%20NL").NavigateTo<Worker>("WorkersWebService");
var workers = await command.FindEntriesAsync();
clemenslinders commented 4 years ago

Hi wintg-zns,

Thanks for your response,

I have one small problem with your code sample.

You use: IBoundClient <Worker

So Worker is a class, but how do you define this class?

Is there an automated way to inherit the correct properties and if not what should it contain.

It would be very nice if you could give me a sample of that.

And how about passing the network credentials in your sample?

wintg-zns commented 4 years ago

@clemenslinders, my apologies for not being more clear.

Worker is the name of your class from your example above. It is the class that represents the object to be retrieved/inserted/updated/deleted. I tried to give an example that followed your example as much as possible.

I'm not quite sure which properties you want to inherit. We have a library that handles/wraps the management of ODataClients and their associated settings. The code looks almost identical to the code above that creates an ODataClientSettings object with a few conditional statements to handle a few options that we configure.

We pass our credentials pretty much the same way you do: clientSettings.Credentials = new NetworkCredential("UserName", "Password");

BC doesn't use anything other than basic auth, right now, but our library has some stubbed code to potentially handle other authentication schemes when BC gets around to implementing them.

clemenslinders commented 4 years ago

Hi Wintg-zns,

Thanks for your reply.

Currently I am on holiday.

In approx 14 days I will give it a try.

Kind regards,

Clemens Linders

clemenslinders commented 4 years ago

Hi Wintg-zns,

Thanks for your reply, I can now say that I have it working.

As there is very little about this topic I will put my whole test code here so that others can benefit from this as well.

AL language: CpWorkers.AL: 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;

            }
        }
    }
}

}

LpWorkers.AL: 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;
            }
        }
    }
}

}

TabWorkers.AL: 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;

}

WorkersWebService.xml: <?xml version = "1.0" encoding = "utf-8" ?>

Page 50108 WorkersWebService true

WorkersWS.xml (not used in this sample) <?xml version = "1.0" encoding = "utf-8" ?>

Page 50109 WorkersWS true

Don't forget to publish the code in D365BC and in D365BC (search for web services) copy the Odata4 address.

C# code: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Simple.OData.Client;

namespace OdataTestSimpleClient { public partial class Form1 : Form { public class Worker { private string No; private string Firstname; private string LastName; private string FunctionName;

        public string no { get => No; set => No = value; }
        public string firstname { get => Firstname; set => Firstname = value; }
        public string lastName { get => LastName; set => LastName = value; }
        public string functionName { get => FunctionName; set => FunctionName = value; }
    }

    public Form1()
    {
        InitializeComponent();
    }

    private async void button1_ClickAsync(object sender, EventArgs e)
    {
        System.Net.NetworkCredential networkCredential = new System.Net.NetworkCredential("USERNAME", "PASSWORD");

        ODataClientSettings clientSettings = new ODataClientSettings("https://api.businesscentral.dynamics.com/v2.0/SomeFunkyGuid/Sandbox/ODataV4/", networkCredential)
        {
            IncludeAnnotationsInResults = true,
            IgnoreResourceNotFoundException = true,
            IgnoreUnmappedProperties = true,
        };

        IODataClient client = new ODataClient(clientSettings);
        IBoundClient<Worker> command = client.For("Company").Key("CRONUS%20NL").NavigateTo<Worker>("WorkersWebService");
        var workers = await command.FindEntriesAsync();
    }
}

}

workers now gets filled with the list of workers that you entered (using the card page) in D365BC.

If you see it like this, it looks very easy, but I tried many many ways and this is the only way that works for ODATA.

I hope more people will find this solution.

Again my thanks to: Wintg-zns.

Kind regards,

Clemens Linders