Search code examples
c#oauth-2.0odatamicrosoft-dynamicsdynamics-365-operations

How to connect to Azure D365 Odata via C#


I am looking for a simple way to connect to an Azure Odata data store (specifically Dynamics365 for Finance and Operations) in a .NET console application (no browser involved) and retrieve odata json. I searched all over MS docs and finally found this:

public OAuthMessageHandler(string serviceUrl, string clientId, string redirectUrl, string username, string password, HttpMessageHandler innerHandler): base(innerHandler) {
  AuthenticationParameters ap = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(serviceUrl + "/data/")).Result;
  AuthenticationContext authContext = new AuthenticationContext(ap.Authority, false);
  AuthenticationResult authResult;
  if (username != string.Empty && password != string.Empty) {

    UserCredential cred = new UserCredential(username, password);
    authResult = authContext.AcquireToken(serviceUrl, clientId, cred);
  } else {
    authResult = authContext.AcquireToken(serviceUrl, clientId, new Uri(redirectUrl), PromptBehavior.Auto);
  }

  authHeader = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}

try/catch below is inside my console app's static main method and calls the function above:

try {
  messageHandler = new OAuthMessageHandler(serviceUrl, clientId, string.Empty, userName, password, new HttpClientHandler());

  using(HttpClient client = new HttpClient(messageHandler)) {
    client.BaseAddress = new Uri(serviceUrl);
    client.Timeout = new TimeSpan(0, 2, 0); //2 minutes
    client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
    client.DefaultRequestHeaders.Add("OData-Version", "4.0");
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    var response = client.GetAsync("api/data/v9.0/WhoAmI", HttpCompletionOption.ResponseHeadersRead).Result;

    if (response.IsSuccessStatusCode) {
      //Get the response content and parse it.  
      JObject body = JObject.Parse(response.Content.ReadAsStringAsync().Result);
      Guid userId = (Guid) body["UserId"];
      Console.WriteLine("Your system user ID is: {0}", userId);
    } else {
      throw new Exception(response.ReasonPhrase);
    }
  }
} catch (Exception ex) {
  DisplayException(ex);
}

IMPORTANT: I am able to successfully connect to D365 F&O in a browser with the username and password that I have. And I am passing those into the function above. I also have a valid clientId. I know all these are valid because I have a 3rd-party app that, with the usn, pwd, clientId, and Authorization URL, I'm able to connect and interact with F&O and its odata. But I'm trying to replicate this interaction as simply as possible in my .NET console app.

I started out with this approach but ended up deviating and mixing in examples and approaches from other docs just to get it all to compile.

Here are my specifics, obfuscated as little as possible:

D365 F&O URL: https://mycustomer2492efd6bdevaos.cloudax.dynamics.com/
Odata URL: https://mycustomer2492efd6bdevaos.cloudax.dynamics.com/data/InventItemPrices
Client Id: c0bzae2e-5233-4465-a2f2-ceb87aa52c3f (I changed some chars)
Authorization URL: https://login.windows.net/mycustomer.com

And I have the username and password. I do not need a redirectUrl so I left that as empty string. I also don't need a client secret. (If I had client secret all this would be much simpler because I found simpler c# code samples to work with that.)

When I call the function above, I get this error:

Invalid authenticate header format - Parameter name: authenticateHeader

The exception is thrown at this line:

AuthenticationParameters ap = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(serviceUrl + "/data/")).Result;

I am not married to the approach I'm using, it's just all I have found that actually compiles and attempts to work. I would greatly appreciate a self-contained function with as few dependencies as possible to connect to my odata and pull down the json as needed.


Solution

  • I finally found the answer:

    string d365RootURL = "https://someenvironmentos.cloudax.dynamics.com";
    string authorizationURL = "https://login.windows.net/something.com";
    var usn = "serviceAcctUsn";
    var pwd = "serviceAcctPwd";
    var clientID = "c0bcae....6aa52c3f";
    string d365OdataURL = d365RootURL + "/data/";
    string odataQuery = "SomeEntity?ItemNumber%20eq%20%27999";
    
    UserCredential creds = new UserPasswordCredential(usn, pwd);
    var authContext = new AuthenticationContext(authorizationURL, false);
    var authTask = authContext.AcquireTokenAsync(d365RootURL, clientID, creds);
    
    try {
      authTask.Wait();
    } catch (Exception ex) {
    
    }
    
    AuthenticationResult authenticationResult = authTask.Result;
    
    var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
    httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
    httpClient.BaseAddress = new Uri(d365OdataURL);
    
    var response = httpClient.GetAsync(odataQuery).Result;