Nachdem in unserem Community Bereich seit längerem diskutiert wird, wie man Dateien ohne Benutzerinaktion nach Business Central in die Cloud laden kann, habe ich mir dieses Thema näher angeschaut.
Ausgangslage
Man hat eine Cloud Version von Business Central und möchte eine oder mehrere Dateien automatisiert einlesen, ohne dass der Benutzer jedes Mal diese Datei über einen Dialog auswählen muss.
Problemstellung
Man hat hier keinen Zugriff auf den Server, der im Hintergrund verwendet wird. Dazu kommt noch, dass nur wenige .net Funktionen direkt in der Programmierumgebung AL zur Verfügung stehen und für die Dateiverarbeitung stehen hier leider nicht die benötigten Funktionen und Befehle zur Verfügung.
Lösung
Da man die Datei(en) nicht direkt auf den Server legen kann, auf dem Business Central als Cloud Version läuft muss man die Dateien auf einen anderen Server legen, der von außerhalb erreichbar ist. Dafür gibt es natürlich verschiedenste Möglichkeiten: ein eigener Server, Sharepoint, AWS, Azure,…
Nachdem wir uns in einem Microsoft Umfeld befinden habe ich mich für Azure entschieden, und zwar Azure File Services.
Weiters brauche ich einen Ersatz für die fehlenden .net Funktionen. Hier habe ich mich für Azure Functions entschieden.
Der Ablauf sieht also in Kürze wie folgt aus:
- Die Datei(en) werden nach Azure File Storage geladen
- Eine Azure Function liest die Datei aus und liefert den Inhalt zurück
- AL Code ruft diese Azure Function auf und verarbeitet das Ergebnis
Voraussetzungen
Um dieses Beispiel umsetzen zu können wird also folgendes benötigt:
- Azure Konto
- Visual Studio Code mit AL Erweiterung und Azure Functions Erweiterung
- Business Central Client (Cloud oder OnPremises)
- Azure Functions Basiskenntnisse
- AL Programmierkenntnisse
Für alle, die sich nicht nicht im Detail mit Azure Functions beschäftigt haben gibt es hier eine praktische Einführung:
Das ganze Videotraining ist hier zu finden:
Azure Functions mit Business Central
Das Beispiel im Detail
Ich habe auf meinem Azure Konto ein Azur File Storage namens „l4d365“ angelegt. Darunter befindet sich ein File Share ebenfalls namens „l4d365“. Innerhalb dieses Fileshares gibt es einen Ordner namens „Dateien“. Und innerhalb dieses Ordners habe ich beispielhaft eine Datei namens „test.txt“ hochgeladen. Das ist eine einfache Textdatei mit dem Inhalt „Das ist ein Test, es hat geklappt, Juhuu!“.
Nun geht es daran eine Azure Function anzulegen. Es handelt sich dabei um eine http-Trigger Function und diese sieht wie folgt aus:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.File;
using Newtonsoft.Json;
namespace DownLoadFile
{public static class DownloadFile
{[FunctionName("DownloadFile")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get","post", Route = null)]
HttpRequest req,ILogger log)
{log.LogInformation("C# HTTP trigger function processed a request.");
string connectionString = req.Query["connectionString"];
string shareName = req.Query["shareName"];
string dirName = req.Query["dirName"];
string fileName = req.Query["fileName"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
connectionString = connectionString ?? data?.connectionString;
shareName = shareName ?? data?.shareName;
dirName = dirName ?? data?.dirName;
fileName = fileName ?? data?.fileName;
if (string.IsNullOrEmpty(connectionString))
{return new BadRequestObjectResult("This HTTP triggered function
executed successfully. Pass a name in the query
string or in the request body for a personalized response.");
}CloudStorageAccount storageAccount = CloudStorageAccount.Parse
(connectionString);
CloudFileClient fileClient = storageAccount.CreateCloudFileClient();
CloudFileShare share = fileClient.GetShareReference(shareName);
if (share.Exists())
{CloudFileDirectory rootDir = share.GetRootDirectoryReference();
CloudFileDirectory sampleDir = rootDir.GetDirectoryReference(dirName);
if (sampleDir.Exists())
{CloudFile file = sampleDir.GetFileReference(fileName);
if (file.Exists())
{return new OkObjectResult(file.DownloadText());
} } }return new BadRequestObjectResult("Fehler im Code");
} } }
Zuletzt lege ich noch die AL Funktion an, die diese Azure Function aufruft:
page 70100 ImportFileFromAzure { PageType = Card; ApplicationArea = All; UsageCategory = Administration; layout { } actions { area(Processing) { action(ActionName) { ApplicationArea = All; trigger OnAction() var client: HttpClient; response: HttpResponseMessage; request: HttpRequestMessage; content: HttpContent; headers: HttpHeaders; TempBlob: Codeunit "Temp Blob"; InStr: InStream; fileContent : Text; begin content.WriteFrom('{"connectionString": "DefaultEndpointsProtoco l=https;AccountName=l4d365;AccountKey=XYZ; EndpointSuffix=core.windows.net","shareName": "l4d365", "dirName": "Dateien","fileName": "Test.txt"}'); content.GetHeaders(headers); headers.Clear(); headers.Add('Content-Type', 'application/json'); request.Content := content; request.SetRequestUri('https://azurefunctionappURL'); request.Method := 'POST'; if client.Send(request, response) then begin response.Content().ReadAs(fileContent); message(fileContent); end; end; } } } }
Wenn alles geklappt hat bekommt man in Business Central die Meldung, die den Dateiinhalt ausgibt.
Hier nochmals das Konzept als Video:
Eine ausführliche Anleitung als Videotraining findet ihr auf unserer Seite:
Dateiupload in Business Central mittels Azure Functions
Das war nun nur einfaches Beispiel das nun auf verschiedenste Weise ausgebaut werden kann, z.B.:
- Mehrere Dateien einlesen
- Binäre Daten wie Bilder statt Text einlesen
- Dateien direkt in die eingehenden Dokumente hängen
- …
Es bleibt also spannend!