D365 – Finance and Operations: OData Integration

D365 - OData Integration

Das Open Data Protocol (OData) ermöglicht die Erstellung von REST-basierten Datendiensten, mit denen Ressourcen, die über Uniform Resource Locators (URLs) identifiziert und in einem Datenmodell definiert sind, von Web-Clients über einfache HTTP-Nachrichten veröffentlicht und bearbeitet werden können.

Diese Spezifikation definiert die Kernsemantik und die Verhaltensaspekte des Protokolls. Microsoft bietet einen OData-REST-Endpunkt an, der alle Datenobjekte bereitstellt, die die interne Property (AOT) des Objektes als ‚IsPublic‘ markiert sind.
Sollte diese Property nicht gesetzt sein, kann man darauf nicht zugreifen. Der Endpunkt unterstützt eine vollständige CRUD-Funktionalität, die das Erstellen, Lesen, Aktualisieren und Löschen von Daten ermöglicht. OData unterstützt die Serialisierung von Daten in verschiedenen Formaten, darunter JSON, XML und ATOM.
Die Standard-Serialisierungsformat ist JSON, aber OData-Clients können auch andere Formate anfordern, indem sie den entsprechenden MIME-Typ angeben. Zudem besteht über Extensions die Möglichkeit in weitere Formate wie CSV und HTML zu serialisieren. Des Weiteren unterstützt es eine Vielzahl von Funktionen, darunter das Filtern, Sortieren und Batching von Daten, sowie eine umfassende Metadaten-Unterstützung.

Folgende Vorteile bietet OData

  1. Steigerung der Interoperabilität: OData folgt einem Standard, Daten zwischen verschiedenen Anwendungen auszutauschen, was die Interoperabilität zwischen Anwendungen verbessert.

  2. Einfache Integrationsmöglichkeit: Mittels REST-Services kann via OData über verschiedene Entitäten hinweg operiert werden.

  3. Vollständige CRUD-Funktionalität: OData ermöglicht eine umfassende Steuerung über Daten, indem es eine vollständige Unterstützung für CRUD-Operationen bietet.

  4. Microsoft-Unterstützung: In vielen MS Produkten (abhängig von Version und Konfiguration) ist die Implementierung von OData durch MS bereitgestellt. So kann durch geeignete Konfiguration bei vielen MS Produkten einfach per OData Zugegriffen werden. Zu den Produkten zählt das hier betrachtete Dynamics 365.

Sie haben Fragen oder möchten weitere Informationen rund um unsere Leistungen und Produkte?
Kontaktieren Sie und jetzt über unser Kontaktformular.

Grundlagen

Adressing

In der folgenden Tabelle wird der Aufbau einer OData URL beschrieben.

Resource URL Description
Service endpoint
[URL]/data/
The root service endpoint for OData entities
Entity collection
[URL]/data/Customers
The collection of all customers
Entity
[URL]/data/Customers("[key]")
A single entity from the entity collection

Supportet Features

OData ist ein umfangreiches Protokoll, welches eine Vielzahl von Features bietet. Hierzu zählen:

 

  1. Query-Unterstützung: Bietet eine umfassende Abfrage-Unterstützung, einschließlich Filtern, Sortieren, Gruppieren, Auswählen von bestimmten Eigenschaften und Aggregation.

  2. Paging: Unterstützt das Paging von Ergebnissen, um die Leistung bei der Verarbeitung großer Datenmengen zu verbessern, mit einer maximalen Seitengröße von 10.000.

  3. Verlinkung: Bietet eine Möglichkeit, Verknüpfungen zwischen Ressourcen herzustellen, um komplexe Datenmodelle zu unterstützen.

  4. Metadaten: Support für umfassende Metadaten-Unterstützung, einschließlich der Fähigkeit, das Schema von Entitäten zu beschreiben und Beziehungen zwischen Entitäten zu definieren.

  5. Atom Pub-Unterstützung: Bietet eine Atom Publishing Protocol (AtomPub) Unterstützung, welches die Veröffentlichung von Ressourcen über RESTful-Webdienste erleichtert.

  6. Batch-Verarbeitung: Ermöglicht die Verarbeitung von Batch-Anfragen. Mit dieser Funktion können mehrere Anforderungen in einer einzelnen Anforderung zusammengefasst werden, was die Leistung verbessert und die Netzwerklatenz reduziert. Batching ist besonders nützlich, wenn eine Anwendung viele kleine Anforderungen an den Server sendet, da dies den Overhead der einzelnen Anforderungen reduziert.

  7. Cross-company(D365 FO): Es werden standardmäßig nur Daten zurück, die zum Standardunternehmen des Benutzers gehören. Um Daten von außerhalb des Standardunternehmens des Benutzers anzuzeigen, geben Sie die Abfrageoption ?cross-company=true an. Diese Option gibt Daten von allen Unternehmen zurück, auf die der Benutzer Zugriff hat. Diese Funktion ist eine D365 Eigenheit und ist kein Standard feature von OData.
    z. B.: http://[baseURI\]/data/FleetCustomers?cross-company=true

Query Options

Eine wichtige Funktion sind die Query Options es bietet die Möglichkeit, Daten zu filtern und zu sortieren. Mit den Query Options können Entwickler komplexe OData Abfragen erstellen, um genau die Daten abzurufen, die sie benötigen. Des Weiteren wird das System dadurch weniger belastet, da nur Daten abfragt werden, die man benötigt.
Folgende Query Options gibt es bei OData.

  1. $filter
    The $filter system query option allows clients to filter a collection of resources that are addressed by a request URL
    • Equals (eq)

    • Not equals (ne)

    • Greater than (gt)

    • Greater than or equal (ge)

    • Less than (lt)

    • Less than or equal (le)

    • And

    • Or

    • Not

    • Addition (add)

    • Subtraction (sub)

    • Multiplication (mul)

    • Division (div)

    • Decimal division (divby)

    • Modulo (mod)

    • Precedence grouping ({ })

    • Wildcard character (*)

				
					https://datapassion.operations.dynamics.com/data/employes?$filter= name eq 'Max Mustermann' 
				
			

2. $count
The count request below returns employes starting with the 19th people of the entity

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$filter= name eq 'Max Mustermann'
				
			

3. $orderby
The $orderby system query option allows clients to request resources in either ascending order using asc or descending order using desc

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$orderby=name desc
				
			

4. $skip
The skip request below returns employes starting with the 19th people of the entity

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$skip=18
				
			

5. $top
The top request below returns the first two people of the entity set.

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$top=2
				
			

6. $expand (only first-level expansion is supported)
The $expand system query option specifies the related resources to be included in line with retrieved resources

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$expand=Project-Managar
				
			

7. $select
The $select system query option allows the clients to requests a limited set of properties

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$select=Name, Job
				
			

8. $search
The $search system query option restricts the result to include only those entities matching the specified search expression

				
					https://datapassion.sandbox.operations.dynamics.com/data/employes?$search=Max
				
			

Actions

Actions können als Funktionen betrachtet werden, die auf Entitäten ausgeführt werden können, um eine bestimmte Aktion auszuführen. Sie können als eine Möglichkeit betrachtet werden, um „was-wäre-wenn“-Szenarien zu unterstützen, bei denen der Benutzer eine Aktion auf eine Entität ausführen möchte, die im Standard-OData-Protokoll nicht unterstützt wird.

Eine Action in OData hat die folgenden Eigenschaften:

  1. Name: Jede Aktion hat einen eindeutigen Namen innerhalb des Dienstes.

  2. Parameter: Actions können Parameter akzeptieren, um spezifische Details zur Verfügung zu stellen, die zur Ausführung der Aktion benötigt werden.

  3. Rückgabe: Actions können eine Rückgabe haben, um dem Benutzer zusätzliche Informationen zur Verfügung zu stellen, die als Antwort auf die Aktion zurückgegeben werden sollen.

  4. Sichtbarkeit: Actions können auf bestimmte Entitäten beschränkt werden, um sicherzustellen, dass sie nur auf bestimmten Ressourcen ausgeführt werden können.

Die genaue Implementierung von Actions in OData ist von der verwendeten OData-Bibliothek und Version abhängig.
Der Code ist ein Beispiel, das auf der Microsoft.AspNet.OData-Version 8.0.1 basiert:

				
					[HttpPost]
public async Task<IActionResult> DoSomething([FromODataUri] int key, ODataActionParameters parameters)
{
    // Code to perform action here

    // Return response with status code 204 (No Content)
    return StatusCode(204);
}

				
			

Authentifizierungsmöglichkeiten

Mithilfe des Nugets können wir die OAuth-Authentifizierung mittels Username-Password-Flow durchgeführt werden.

Vorbereitende Maßnahmen / Voraussetzung (optional)

Authentifizierung

Die Authentifizierung erfolgt über OAuth (Connected App) und ist in unserem Beitrag D365 – FINANCE AND OPERATIONS: EFFIZIENTE INTEGRATION FÜR OPTIMALE PROZESSE beschrieben.

Limitierungen

Es gibt zwei Arten von API-Limits zum Schutz von D365 FO: benutzerbasiert und ressourcenbasiert. Benutzerbasierte Limits verhindern, dass einzelne Benutzer oder Integrationen die Systemleistung und -verfügbarkeit beeinträchtigen. Ressourcenbasierte Limits tragen zum Schutz der Umgebung bei, indem sie Schwellenwerte für eine hohe Auslastung der Umgebungsressourcen erzwingen. Wenn hohe Schwellenwerte erreicht werden, werden Serviceanfragen begrenzt.
Die Limitierung werden von dem Unternehmen der Umgebung festgelegt.

 

Die folgende Tabelle beschreibt die standardmäßigen benutzerbasierten API-Grenzwerte für den Dienstschutz, die pro Benutzer, pro Anwendungs-ID und pro Webserver durchgesetzt werden.

Measure Description Service protection limit
Number of requests
The cumulative number of requests that the user has made.
6,000 within the five-minute sliding window
Execution time
The combined execution time of all requests that the user has made.
20 minutes (1,200 seconds) within the five-minute sliding window
Number of concurrent requests
The number of concurrent requests that the user has made.
52

Code-Beispiele

Als Code-Beispiel wird im Folgenden beschrieben, wie man eine OData-Verbindung in Richtung Dynamics 365 for Finance and Operations (D365 FO) aufbauen kann.

Konfiguration der Settings für den FoServiceClient

Für die Konfiguration der FoServiceClientsSettings des FoServiceClients benötigt es lediglich eine Property vom Typ FoServiceClientsSettings in der Settings classe.
Durch Vererbung des IFoServiceClientsSettings-Interfaces werden alle erforderlichen Propertys in die Settings classe vererbt.

				
					 public class Settings : IFoServiceClientSettings
    {
        public FoServiceClientSettings FoServiceClientSettings{ get; set; }
    }

				
			
				
					public class FoServiceClientSettings
    {
        /// <summary>
        /// Azure active directory service principal Application (client) ID
        /// </summary>
        public string ClientId { get; init; }

        /// <summary>
        /// Azure active directory service principal client secret
        /// </summary>
        public string ClientSecret { get; init; }

        /// <summary>
        /// CRM Resource ID [CRM Environment URL] liek ****.crm4.dynamics.com/
        /// </summary>
        public string EnvironmentUrl { get; init; }

        /// <summary>
        /// Azure tenant id
        /// </summary>
        public string TenantId { get; init; }

        /// <summary>
        /// Timeout for web api request. Default value is 5 minutes.
        /// </summary>
        public int TimeoutInMinutes { get; init; } = 5;

        /// <summary>
        /// Caching the Acceskey  
        /// </summary>
        public bool UseAccesTokenCache { get; init; }
    }
				
			

Die Konfiguration für die FoServiceClientSettings könnte wie folgt aussehen:

				
					{
	"Projekt:ClientId": "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
	"Projekt:ClientSecret": "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
	"Projekt:EnvironmentUrl": "https://baseUrl/data",
	"Projekt:TenantId": 0000000000000,
	"Projekt:TimeoutInMinutes": 10,
}
				
			

Implementierung des FoServiceClient

Der FoServiceClient stellt einen Service bereit, der über einen HttpClient eine Verbindung zu Dynamics 365 Finance and Operations herstellt, um den Datenaustausch mit D365 zu ermöglichen. Der Service bietet die CURD-Operationen (Create, Update, Read und Delete) Funktionen an. In dem folgenden Codebeispiel wird nur die Read der CURD-Operationen vorgestellt.

  • Die GetEntity-Methode nimmt ein OData-Query entgegen, das an die Methode SendRequestList<T>(requestUri, HttpMethod.Get) weitergeleitet wird. In dieser Methode wird das Query mithilfe des HttpClient als Anfrage an den D365 OData-Endpunkt gesendet. Sofern keine Verarbeitungsfehler auftreten, gibt die Methode das <T>-Objekt zurück, das die entsprechende Übergabe Entität repräsentiert.

Des Weiteren gibt es im FoServiceClient noch zwei Hilfsmethoden:

  • Die CreateClient Methode, ist für die Erstellung des HttpClient. Diese Methode erzeugt einen neuen HttpClient, der verwendet werden kann, um eine Verbindung zu einem bestimmten Ressourcen-URI herzustellen. Der HttpClient ermöglicht dann den Datenaustausch mit D365 FO über HTTP-Anfragen.

  • Die FetchToken Methode, ist für das Abrufen des Tokens zur Authentifizierung in D365 FO zuständig. Diese Methode handhabt den Authentifizierungsprozess, um einen gültigen Zugriffstoken von D365 FO zu erhalten. Das Token wird verwendet, um die Anfragen an D365 FO zu autorisieren und den Zugriff auf die geschützten Ressourcen zu ermöglichen.

				
					using Common.Connectors.FoServiceClient.ConnectorSettings;
using Common.Connectors.FoServiceClient.Entities;
using Common.Exceptions;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Common.Connectors.FoServiceClient.Services;
using WorkOrder.D365FS.Out.D365FO.Connector.ExternalAccess.Interfaces;
using Microsoft.Extensions.Caching.Memory;
using Serilog;

namespace WorkOrder.D365FS.Out.D365FO.Connector.Services
{
    public class FoServiceClient : IFoServiceClient
    {
        private readonly FoServiceClientSettings foServiceClientSettings;
        private readonly IMemoryCache cache;
        private const string TOKENNAME = "D365TOKEN";
        private readonly Httpclient client;


        public FoServiceClient(IOptions<Settings> options, IMemoryCache memoryCache)
        {
            this.foServiceClientSettings = options.Value.FoServiceClientSettings;
            cache = this.foServiceClientSettings.UseAccesTokenCache ? memoryCache : null;
            client = this.CreateClient(this.foServiceClientSettings.EnvironmentUrl)
        }
              
        /// <summary>
        /// Get entity
        /// </summary>
        /// <typeparam name="T">Generic parameter</typeparam>
        /// <param name="requestUri">Request Uri</param>
        /// <returns>Type that is defined for generic T </returns>
        public async Task<T> GetEntity<T>(string requestUri) where T : class
        {
            Log.Information($"Getting {typeof(T)} entites from Finance and Operations.");
            var oDataResponse = await this.SendRequestList<T>(requestUri, HttpMethod.Get)
                .ConfigureAwait(false);

            if (oDataResponse.Value.Any())
            {
                if(oDataResponse.Value.Count > 1)
                {
                    throw new UnrecoverableArgumentException($"The Expected output of the odata response was more then 1, output records:{oDataResponse.Value.Count}");
                }
                else
                {
                    return oDataResponse.Value.First();
                }             
            }
            else
            {
                return new T();
            }
        }
              
        private async Task<OdataResponseList<T>> SendRequest<T>(string requestUri, HttpMethod httpMethod, T body = null) where T : class
        {
                var accessToken = await FetchToken();
                var request = new HttpRequestMessage(httpMethod, requestUri);
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

                if (HttpMethod.Post == httpMethod || HttpMethod.Patch == httpMethod)
                {
                    request.Headers.Add("Prefer", "return=representation");
                    request.Content = new StringContent(
                        JsonConvert.SerializeObject(body),
                        Encoding.UTF8,
                        "application/json");
                }

                var response = client.SendAsync(request).Result;
                try
                {
                    switch (response.StatusCode)
                    {
                        case HttpStatusCode.OK:
                        case HttpStatusCode.Created:
                            Log.Information($"Fo web api responded with status code {response.StatusCode} and reason pharse {response.ReasonPhrase}.");
                            var ODataResponseFo = JsonConvert.DeserializeObject<OdataResponseList<T>>(await response.Content.ReadAsStringAsync());
                            if (ODataResponseFo.Value is null)
                            {
                                ODataResponseFo.Value = JsonConvert.DeserializeObject<List<T>>(await response.Content.ReadAsStringAsync());
                            }
                            return ODataResponseFo;
                            
                        case HttpStatusCode.TooManyRequests:
                            Log.Information($"Fo web api responded with status code {response.StatusCode} and reason pharse {response.ReasonPhrase}.");
                            var deltaInSeconds = response.Headers.RetryAfter.Delta ?? throw new Exception();

                            Log.Information($"Fo web api server is busy, waiting for {deltaInSeconds} seconds to send a new request.");
                            await Task.Delay(deltaInSeconds);
                            return await SendRequestList<T>(requestUri, httpMethod, body);

                        default:
                            var detailedErrorMessage = response.Content.ReadAsStringAsync().Result;
                            if (detailedErrorMessage.Contains("0x80043b06", StringComparison.OrdinalIgnoreCase))
                            {
                                throw new UnrecoverableException($"Datverse responded for {typeof(T)} with status code: {response.StatusCode} and reason pharse: {response.ReasonPhrase}, detailed error message {detailedErrorMessage}");
                            }
                            else
                            {
                                throw new InvalidOperationException($"Datverse responded for {typeof(T)} with status code: {response.StatusCode} and reason pharse: {response.ReasonPhrase}, detailed error message {detailedErrorMessage}");
                            }
                    }
                }
                catch (Exception ex)
                {

                    throw new UnrecoverableException($"Datverse responded for {typeof(T)} with status code: {ex} and reason pharse: {response.ReasonPhrase}, detailed error message ");
                }            
        }

        private HttpClient CreateClient(string resourceUri)
        {
            var zeroHours = 0;
            var zeroSeconds = 0;

            var client = new HttpClient
            {
                BaseAddress = new Uri(resourceUri),
                Timeout = new TimeSpan(
                    zeroHours,
                    foServiceClientSettings.TimeoutInMinutes,
                    zeroSeconds)
            };

            client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
            client.DefaultRequestHeaders.Add("OData-Version", "4.0");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            return client;
        }

        /// <summary>
        /// Fetching token with caching
        /// </summary>
        /// <returns>Return the token</returns>
        private async Task<string> FetchToken()
        {
            string accesToken;

            if (cache != null)
            {
                if (!cache.TryGetValue(TOKENNAME, out accesToken))
                {
                    var tokenResult = await OAuthAccessToken.GetTokenAsync(foServiceClientSettings);

                    var options = new MemoryCacheEntryOptions()
                        .SetAbsoluteExpiration(tokenResult.ExpiresOn);
                    cache.Set(TOKENNAME, tokenResult.AccessToken, options);
                    accesToken = tokenResult.AccessToken;
                }
            }
            else
            {
                var tokenResult = await OAuthAccessToken.GetTokenAsync(foServiceClientSettings);
                accesToken = tokenResult.AccessToken;
            }
            return accesToken;
        }
    }
}
				
			

Implementierung des IFoServiceClient Interfaces

Für die Implementierung des FoServiceClients benötigen wir auch noch das IFoServiceClient-Interface, das es uns ermöglicht, eine lose Kopplung zwischen Klassen und dem Interface herzustellen.
Durch das Verwendung des IFoServiceClient-Interfaces können Klassen die erforderlichen Methoden und Funktionen implementieren, um mit dem FoServiceClient zu interagieren, ohne spezifische Details der anderen Klassen zu kennen.

				
					using System.Threading.Tasks;
using System.Collections.Generic;

namespace Common.Connectors.FoServiceClient.Interfaces
{
    public interface IFoServiceClient
    {
        Task<T> Get<T>(string requestUri) where T : class;
    }
}

				
			

Die Klasse OAuthAccessToken ist eine Hilfsklasse, die für die Generierung und den Abruf von OAuth-Access-Tokens für eine Anwendung konzipiert ist. Dabei verwendet sie die Azure Active Directory-Authentifizierung (Azure AD).
OAuth 2.0 ist ein offenes Autorisierungsprotokoll, das es ermöglicht, den Zugriff einer Anwendung auf Ressourcen eines Benutzers in einem bestimmten Dienst zu gewähren, ohne dass der Benutzer seine Anmeldeinformationen preisgeben muss.

				
					  public static class OAuthAccessToken
    {
        public static async Task<AppAuthenticationResult> GetTokenAsync(FoServiceClientSetting ssettings)
        {
            var connectionString = CreateConnectionString(
                settings.ClientId,
                settings.ClientSecret,
                settings.TenantId);

            var azureServiceTokenProvider = new AzureServiceTokenProvider(connectionString);
            var tokenresponse = await azureServiceTokenProvider
                .GetAuthenticationResultAsync(settings.EnvironmentUrl)
                .ConfigureAwait(false);

            return tokenresponse;
        }

        private static string CreateConnectionString(string clientId, string clientSecret, string tenantId)
        {
            return $"RunAs=App;AppId={clientId};TenantId={tenantId};AppKey={clientSecret}";
        }
    }
				
			

Hinzufügen in der Proramm.cs

Last but not least muss der FoServiceClient zur Solution hinzugefügt werden. Dies geschieht in der Programm.cs, indem ein neuer Service mit den entsprechenden Propertys erstellt wird und dieser zur Liste der Services hinzugefügt wird.

z. B: Für eine NSB Anwendung

				
					  public static void Main()
        {
            GenericHostCreator.Create<Settings>(
                new EndpointSpecification(
                    isSendOnly: false,
                    usePersistence: true,
                    useAzureDataBus: false,
                    useMonitoring: false,
                    solutionName: SolutionConstants.SolutionName,
                    commandRoutings: CommandRoutings.Routings,
                    services: new List<Service>
                    {
                         new Service
                        {
                            ServiceType = typeof(FoServiceClient<Settings>),
                            InterfaceType = typeof(IFoServiceClient),
                            ServiceLifecycle = Lifecycle.Singleton
                        }
                    },
                    endpointBehaviors: null));
        }
				
			

z.B: Für ein Allgemein gültige Webanwendung

				
					var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add the FoServiceClient as a singelton service
builder.Services.AddSingleton<IFoServiceClient, FoServiceClient<Settings>>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

				
			

Sie haben Fragen oder möchten weitere Informationen rund um unsere Leistungen und Produkte?
Kontaktieren Sie und jetzt über unser Kontaktformular.

Data Passion Milen & Carsten
Data Passion Milen & Carsten
HABEN WIR
IHR INTERESSE
GEWECKT?
Schreiben Sie uns eine kurze E-Mail oder rufen Sie uns einfach an! Wir kümmern uns um Ihr Anliegen innerhalb der nächsten 24 Stunden.

Tel.: +49 (40) 6963816–0
Tel.: +49 (151) 1176898-0
E-Mail: [email protected]

Kontaktanfrage

Kontaktanfrage