← Tous les articles

Azure Functions et Business Central : synchroniser un planificateur dans les deux sens

Par Fred Morin ·

Aussi en anglais : Azure Functions and Business Central: syncing a scheduler both ways

Partager cet articleLinkedInFacebookCourriel

Dans l'article précédent, j'ai décrit un planificateur de ressources en glisser-déposer qui roulait dans une page Business Central. Celui-ci couvre la plomberie derrière : comment un déplacement fait dans le planificateur se rendait dans Business Central, et comment les deux restaient synchronisés, grâce à des Azure Functions.

Les données : des réservations liées aux lignes de commande

Dans Business Central, chaque réservation était liée à une ligne de commande de vente portant une ressource. C'est un choix naturel : le travail planifié, c'est ce qui finit sur la facture, donc la réservation et la ligne facturable restent ensemble.

Deux sortes de données portaient l'horaire :

  • une table d'horaire dans l'extension, avec l'heure de début et de fin de chaque réservation ;
  • quelques champs ajoutés à la ligne de vente (Sales Line) par une extension de table, pour les valeurs de planification qui allaient avec la ligne de commande elle-même.

Quand un utilisateur glissait une réservation à une nouvelle heure, ce qui devait changer dans Business Central, c'était le début et la fin dans cette table d'horaire.

Pourquoi des Azure Functions au milieu

Le planificateur (une application Blazor) aurait pu, en principe, appeler Business Central directement. Une couche d'Azure Functions entre les deux est un bon endroit pour tout ce qui parle à Business Central : l'authentification, les reprises, la journalisation, et la traduction entre ce que le planificateur connaît et ce que l'API de Business Central attend. Elle vivait aussi à côté du reste du côté Azure, qui gérait la synchronisation avec l'application mobile Xamarin des vétérinaires sur la route.

Planificateur → Functions → Business Central

Quand une réservation bougeait, l'application Blazor appelait une Function à déclencheur HTTP, qui appelait l'API sur mesure de l'extension pour mettre à jour le début et la fin. Une version simplifiée, pas le code d'origine :

public record BookingMove(Guid ScheduleEntryId, DateTime Start, DateTime End);

public class MoveBooking(BusinessCentralClient bc)
{
    [Function("MoveBooking")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "bookings/move")] HttpRequestData req)
    {
        var move = await req.ReadFromJsonAsync<BookingMove>();
        if (move is null) return req.CreateResponse(HttpStatusCode.BadRequest);

        await bc.UpdateScheduleAsync(move);
        return req.CreateResponse(HttpStatusCode.NoContent);
    }
}

Et l'appel à Business Central, un PATCH sur un enregistrement de la page API sur mesure :

public async Task UpdateScheduleAsync(BookingMove move)
{
    using var request = new HttpRequestMessage(HttpMethod.Patch, $"{_apiBase}/scheduleEntries({move.ScheduleEntryId})")
    {
        Content = JsonContent.Create(new { startDateTime = move.Start, endDateTime = move.End })
    };
    // Business Central exige If-Match sur les mises à jour. « * » veut dire
    // « peu importe la version en place » ; voir la note sur la concurrence plus bas.
    request.Headers.IfMatch.Add(EntityTagHeaderValue.Any);

    using var response = await _http.SendAsync(request);
    response.EnsureSuccessStatusCode();
}

Business Central → Functions

Le trafic allait dans les deux sens. Du code AL dans Business Central appelait aussi les Functions, en HTTP tout simple. En AL, c'est le HttpClient intégré, avec la clé de la Function dans l'en-tête x-functions-key. Par exemple :

procedure NotifyScheduler(ScheduleEntry: Record "Schedule Entry")
var
    Setup: Record "Scheduler Setup";
    Client: HttpClient;
    Content: HttpContent;
    Headers: HttpHeaders;
    Response: HttpResponseMessage;
    Body: JsonObject;
    BodyText: Text;
    FunctionKey: Text;
begin
    if not Setup.Get() or not Setup.Enabled then
        exit;
    // Les secrets vont dans IsolatedStorage, pas dans un champ de table lisible par les utilisateurs.
    if not IsolatedStorage.Get('SchedulerFunctionKey', DataScope::Module, FunctionKey) then
        Error('The scheduler key is not set up.');

    Body.Add('scheduleEntryId', Format(ScheduleEntry.SystemId, 0, 4).ToLower());
    Body.Add('start', ScheduleEntry."Start DateTime");
    Body.Add('end', ScheduleEntry."End DateTime");
    Body.WriteTo(BodyText);

    Content.WriteFrom(BodyText);
    Content.GetHeaders(Headers);
    Headers.Remove('Content-Type');
    Headers.Add('Content-Type', 'application/json');
    Client.DefaultRequestHeaders.Add('x-functions-key', FunctionKey);

    if not Client.Post(Setup."Functions URL" + '/api/bookings/changed', Content, Response) then
        Error('The scheduler could not be reached.');
    if not Response.IsSuccessStatusCode then
        Error('The scheduler returned %1.', Response.HttpStatusCode);
end;

Des minuteries pour tout ce qui n'est pas urgent

Tout n'a pas besoin d'arriver à l'instant où l'utilisateur clique. À côté des déclencheurs HTTP, des Functions à déclencheur de minuterie roulaient selon un horaire pour garder les deux côtés alignés : le genre de synchronisation de fond où quelques minutes de délai ne changent rien.

[Function("SyncSchedule")]
public async Task Run([TimerTrigger("0 */5 * * * *")] TimerInfo timer)
{
    // Aux cinq minutes : prendre ce qui a changé de chaque côté et réconcilier.
    await _sync.RunAsync();
}

La règle est pratique : HTTP quand une personne attend le résultat, minuterie quand personne n'attend.

La performance

C'était pas mal rapide. Un glisser-déposer dans le planificateur faisait l'aller-retour par la Function jusqu'à l'API de Business Central sans que les utilisateurs attendent.

À savoir si vous construisez ça

Des conseils généraux, pas une liste de problèmes que j'ai eus :

  • La concurrence. If-Match: * écrase ce qui est là. Si deux personnes peuvent modifier la même réservation, envoyez plutôt le @odata.etag de l'enregistrement, et gérez le 412 que Business Central renvoie quand quelqu'un est passé avant.
  • L'authentification à Business Central. Pour Business Central en ligne, utilisez une inscription d'application Microsoft Entra avec accès de service à service, et gardez son secret dans Azure Key Vault ou les paramètres de la Function, jamais dans le code.
  • Les champs ajoutés et les commandes libérées. Bien des champs standard de la ligne de vente refusent les changements une fois la commande libérée, parce que leur validation appelle TestStatusOpen. Vos propres champs ne le font pas, à moins que vous l'ajoutiez : décidez exprès si une réservation peut bouger sur une commande libérée.
  • L'idempotence. Les minuteries et les reprises font qu'un même changement peut arriver deux fois. Faites en sorte que chaque mise à jour puisse être appliquée de nouveau sans dégât.
  • La journalisation. Quand une synchronisation tourne mal, les Functions sont le seul endroit qui a vu les deux côtés. Journalisez assez pour reconstituer ce qui s'est passé.

Deux systèmes qui devraient s'entendre et ne s'entendent pas? Parlons-en.

Partager cet articleLinkedInFacebookCourriel