Skip to main content

Notifications

Consumir Entidades de Dynamics 365 F&O desde C#

Juan Antonio Tomas Profile Picture Juan Antonio Tomas 236 User Group Leader

En esta ocasión, vengo a compartir una pieza de código que me ha sido muy útil a la hora de consumir entidades de datos de Dynamics 365 Finance and Operations, ya sean tanto entidades estándar, como entidades personalizadas, desde cualquier tipo de aplicación escrita en C#. En mi caso concreto, es una pieza de código escrita para ser consumida desde Azure Functions.

Como muchos de vosotros sabréis, Microsoft tiene a nuestra disposición, ejemplo de código en su repositorio de GitHub para consumir estas entidades utilizando OData Client. Es un ejemplo más que válido, que he utilizado más de una vez, y que además recomiendo encarecidamente para aprender el funcionamiento de las mismas, así como la autenticación a través de Azure Active Directory, pero en mi caso particular, decidí hacer este helper con llamadas http a través de HttpClient estándar de .NET con el objetivo de generar un código mucho más ligero, sin necesidad de generar la gran cantidad de clases proxy que genera el cliente de OData para poder utilizar todas las entidades de Dynamics 365 F&O.

Como ya sabrás a estas alturas de la historia, no soy experto en código C#, ni siquiera estoy cerca de serlo, por lo tanto, si a lo largo de este post ves algún punto de mejora, estaré más que agradecido de que me dejes un comentario con el objetivo de mejorarlo :).

Una consideración a tener en cuenta cuando escribimos código para consumir entidades de datos, es que podemos recibir un error 429 en cualquier momento, debido al throttling priority, por lo tanto, asegúrate desarrollar un patrón de reintentos coherente para gestionarlo.

Sin más dilación, me dispongo a mostrar el código genérico que he escrito para poder utilizar de forma genérica con cualquiera de las entidades existentes en el sistema, que como ya sabes, no son pocas.

Como verás a continuación, he desarrollado un método para cada una de las operaciones que podemos realizar. Estas son GET, POST, PATCH y DELETE.

Autenticación

El primer paso para poder consumir entidades de datos, conseguir el token de acceso a través de la autenticación mediante Azure Active Directory. Para ello, necesitaremos generar un registro de aplicación en nuestro tenant. Para ello, puedes seguir las instrucciones que escribí en este post.

Seguidamente, generamos un contrato de datos, que nos permita interactuar de forma sencilla con la respuesta que obtengamos al obtener el token.

[DataContract]
public class TokenDC
{
    [DataMember]
    public string token_type { get; set; }
    [DataMember]
    public string expires_in { get; set; }
    [DataMember]
    public string ext_expires_in { get; set; }
    [DataMember]
    public string expires_on { get; set; }
    [DataMember]
    public string not_before { get; set; }
    [DataMember]
    public string resource { get; set; }
    [DataMember]
    public string access_token { get; set; }
}

Ahora ya podemos ver, de forma sencilla, el método que utilizaremos para obtener el token de acceso, donde:
domain es tu tenant de Azure, por ejemplo, jatomas.com
clientId es el id de cliente de tu App Registration
clientSecret es el secreto generado en la App Registration
resource la url de tu instancia de F&O (sin la barra final ‘/’)

public static string GetToken()
{
    try
    {
        var domain = "tenant_azure"
        var clientId = "aad_client_id";
        var clientSecret = "aad_client_secret";
        var resource = "url_d365fo_without_bar";

        HttpClient client = new HttpClient();

        string requestUrl = $"https://login.microsoftonline.com/{domain}/oauth2/token";

        string request_content = $"grant_type=client_credentials&resource={resource}&client_id={clientId}&client_secret={clientSecret}";

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
        try
        {
            request.Content = new StringContent(request_content, Encoding.UTF8, "application/x-www-form-urlencoded");
        }
        catch (Exception ex)
        {
            var msg = ex.Message;
        }
        HttpResponseMessage response = client.SendAsync(request).Result;

        string responseString = response.Content.ReadAsStringAsync().Result;
        var token = JSONSerializer<TokenDC>.DeSerialize(responseString);
        var accessToken = token.access_token;
        return accessToken;
    }
    catch (Exception ex)
    {
        var message = $"There was an error retrieving the token:\r\n{ex.Message}";
        throw new Exception(message);
    }
}

Una vez que tenemos el token de acceso, ya podemos continuar, realizando la operación que necesitemos. A continuación, os dejo el método que utilizaremos para cada uno de estas operaciones.

GET (Select)

// GET https://jatomas.operations.dynamics.com/CustomersV3(dataAreaId='USMF',CustomerAccount='JAT0001')?cross-company=true
public static string GetEntity(string dataEntityName, string dataEntityKey)
{
    HttpClient client = new HttpClient();

    //Get authorization token
    var erpToken = GetToken();

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", erpToken);
    var endpointUrl = "url_d365fo_without_bar";
    endpointUrl = endpointUrl + "/data/" + dataEntityName + dataEntityKey + "?cross-company=true";

    string responseString = string.Empty;
    int retries = 0;
    int seconds = RetrySeconds;

    for (; ; )
    {
        try
        {
            retries++;

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, endpointUrl);
            HttpResponseMessage response = client.SendAsync(request).Result;

            responseString = response.Content.ReadAsStringAsync().Result;

            if (!response.IsSuccessStatusCode)
            {
                if ((int)response.StatusCode == 429 && retries < MaxRetries)
                {
                    //Try to use the Retry-After header value if it is returned. 
                    if (response.Headers.Contains("Retry-After"))
                    {
                        seconds = int.Parse(response.Headers.GetValues("Retry-After").FirstOrDefault());
                    }

                    Thread.Sleep(TimeSpan.FromSeconds(seconds));
                    continue;
                }
                else if ((int)response.StatusCode == 404)
                {
                    // Entity not found, don't retry
                    return responseString;
                }
                else
                {
                    throw new Exception(responseString);
                }
            }

            return responseString;
        }
        catch (Exception ex)
        {
            string message = $"There was an error when trying to get the {dataEntityName} with the key {dataEntityKey}:\r\n{ex.Message}";
            throw new Exception(message);
        }
    }
}

Uso del método GetEntity

GetEntity("CustomersV3", "(dataAreaId='USMF',CustomerAccount='JAT0001')");

En el caso del GET, comentar que hay distintas formas de llamarlo. La que yo estoy utilizando (https://fnourl.com/data/DataEntity(EntityKey=’Value’) la utilizaremos siempre y cuando estemos buscando un registro concreto, a través de su clave de entidad. Sería lo más parecido a los métodos find que utilizamos en X++, pero también tenemos la opción de obtener varios registros filtrando por los campos que queramos, dependiendo de la información con la que contemos, siguiendo la siguiente nomenclatura: https://fnourl.com/data/DataEntity?$filter=Campo1 eq ‘Value’ and Campo2 eq ‘Value’. La diferencia a la hora de obtener los datos es que, en lugar de obtener un objeto único, obtendremos un array con todos los registros que cumplan las condiciones indicadas en el $filter.

POST (Insert)

// POST https://jatomas.operations.dynamics.com/CustomersV3
// {
//      "dataAreaId":"USMF",
//      "CustomerAccont":"JAT001",
//      "CustomerGroupId":"NAC"
// }
public static string InsertEntity(string dataEntityName, string requestContent)
{
    HttpClient client = new HttpClient();

    //Get authorization token
    var erpToken = GetToken();

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", erpToken);
    var endpointUrl = "url_d365fo_without_bar";
    endpointUrl = endpointUrl + "/data/" + dataEntityName;

    string responseString = string.Empty;
    int retries = 0;
    int seconds = RetrySeconds;
            
    for ( ; ; )
    { 
        try
        {
            retries++;

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, endpointUrl);

            request.Content = new StringContent(requestContent, Encoding.UTF8, "application/json");

            HttpResponseMessage response = client.SendAsync(request).Result;
                
            responseString = response.Content.ReadAsStringAsync().Result;

            if (!response.IsSuccessStatusCode)
            {
                if ((int)response.StatusCode == 429 && retries < MaxRetries)
                {
                    //Try to use the Retry-After header value if it is returned. 
                    if (response.Headers.Contains("Retry-After"))
                    {
                        seconds = int.Parse(response.Headers.GetValues("Retry-After").FirstOrDefault());
                    }

                    Thread.Sleep(TimeSpan.FromSeconds(seconds));
                    continue;
                }
                else
                { 
                    throw new Exception(responseString);
                }
            }

            return responseString;
        }
        catch (Exception ex)
        {
            string message = $"There was an error when trying to insert into {dataEntityName}:\r\n{ex.Message}";
            throw new Exception(message);
        }
    }
}

Uso del método InsertEntity

string content = @"{
      "dataAreaId":"USMF",
      "CustomerAccont":"JAT001",
      "CustomerGroupId":"NAC"
 }";
InsertEntity("CustomersV3", content);

PATCH (Update)

// PATCH https://jatomas.operations.dynamics.com/CustomersV3(dataAreaId='USMF',CustomerAccount='JAT0001')?cross-company=true
// {
//      "fieldToUpdate":"newValue"
// }
public static string UpdateEntity(string dataEntityName, string dataEntityKey, string requestContent)
{
    HttpClient client = new HttpClient();

    //Get authorization token
    var erpToken = GetToken();

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", erpToken);
    var endpointUrl = "url_d365fo_without_bar";
    endpointUrl = endpointUrl + "/data/" + dataEntityName + dataEntityKey + "?cross-company=true";

    string responseString = string.Empty;
    int retries = 0;
    int seconds = RetrySeconds;

    for (; ; )
    {
        try
        {
            retries++;

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, endpointUrl);

            request.Content = new StringContent(requestContent, Encoding.UTF8, "application/json");

            HttpResponseMessage response = client.SendAsync(request).Result;

            responseString = response.Content.ReadAsStringAsync().Result;

            if (!response.IsSuccessStatusCode)
            {
                if ((int)response.StatusCode == 429 && retries < MaxRetries)
                {
                    //Try to use the Retry-After header value if it is returned. 
                    if (response.Headers.Contains("Retry-After"))
                    {
                        seconds = int.Parse(response.Headers.GetValues("Retry-After").FirstOrDefault());
                    }

                    Thread.Sleep(TimeSpan.FromSeconds(seconds));
                    continue;
                }
                else
                {
                    throw new Exception(responseString);
                }
            }

            return responseString;
        }
        catch (Exception ex)
        {
            string message = $"There was an error when trying to update into {dataEntityName}:\r\n{ex.Message}";
            throw new Exception(message);
        }
    }
}

Uso del método UpdateEntity

string content = @"{
      "CustomerGroupId":"INT"
 }";
UpdateEntity("CustomersV3","(dataAreaId='USMF',CustomerAccount='JAT0001')" , content);

DELETE (Delete)

// DELETE https://jatomas.operations.dynamics.com/CustomersV3(dataAreaId='USMF',CustomerAccount='JAT0001')?cross-company=true
public static string DeleteEntity(string dataEntityName, string dataEntityKey)
{
    HttpClient client = new HttpClient();

    //Get authorization token
    var erpToken = GetToken();

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", erpToken);
    var endpointUrl = "url_d365fo_without_bar";
    endpointUrl = endpointUrl + "/data/" + dataEntityName + dataEntityKey + "?cross-company=true";

    string responseString = string.Empty;
    int retries = 0;
    int seconds = RetrySeconds;

    for (; ; )
    {
        try
        {
            retries++;

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, endpointUrl);

            HttpResponseMessage response = client.SendAsync(request).Result;

            responseString = response.Content.ReadAsStringAsync().Result;

            if (!response.IsSuccessStatusCode)
            {
                if ((int)response.StatusCode == 429 && retries < MaxRetries)
                {
                    //Try to use the Retry-After header value if it is returned. 
                    if (response.Headers.Contains("Retry-After"))
                    {
                        seconds = int.Parse(response.Headers.GetValues("Retry-After").FirstOrDefault());
                    }

                    Thread.Sleep(TimeSpan.FromSeconds(seconds));
                    continue;
                }
                else
                {
                    throw new Exception(responseString);
                }
            }

            return responseString;
        }
        catch (Exception ex)
        {
            string message = $"There was an error when trying to delete into {dataEntityName}:\r\n{ex.Message}";
            throw new Exception(message);
        }
    }
}

Uso del método DeleteEntity

DeleteEntity("CustomersV3", "(dataAreaId='USMF',CustomerAccount='JAT0001')");

Conclusión

Hasta aquí el post de hoy, espero que os resulte de utilidad, y como decía al principio, cualquier duda, crítica (constructiva) o mejora del mismo, estaré encantado de leeros en los comentarios. Saludos!

Comments

*This post is locked for comments