Hi, Readers.
We discussed Dynamics 365 Business Central: How to use OAuth 2.0 in AL with SecretText (Using codeunit 501 OAuth2) a few weeks ago. Today I would like to briefly introduce a requirement that is often asked, how to import/read files from OneDrive to Business Central via AL (Graph API).
OneDrive for work or school (formerly known as OneDrive for Business) is a cloud storage service that is included in Microsoft 365. Business Central makes it easy to store, manage, and share files with other people through OneDrive. We have discussed these standard features before.
- Business Central 2021 wave 2 (BC19): AL API for sharing files in Microsoft 365 (Cloud Storage and Open in OneDrive)
- Implement your custom “Open in OneDrive” action in Dynamics 365 Business Central
- Business Central 2022 wave 1 (BC20): Share a file through OneDrive (new Share action)
- Business Central 2023 wave 1 (BC22): Edit in OneDrive
However, the standard has not been able to achieve automation, such as automatically reading files from OneDrive into Business Central. While this can be done with Power Automate, it is not very convenient as standard APIs are not scalable and developers need to maintain multiple systems.
- How to automatically upload files from OneDrive to Business Central with Power Automate (No customization)
- Create a flow in Power Automate to update multiple customer pictures in Business Central from OneDrive
So, is it possible to do this through AL Code? Yes, we can use Microsoft Graph API.
Microsoft Graph API: Microsoft Graph is a RESTful web API that enables you to access Microsoft Cloud service resources. After you register your app and get authentication tokens for a user or service, you can make requests to the Microsoft Graph API.
Registering your application establishes a trust relationship between your app and the Microsoft identity platform. This part of the setup is the same as in Using OAuth to connect Business Central APIs and Web Service in Postman. Just the API permissions are different.
To configure application permissions for the app in the app registrations experience on the Microsoft Entra admin center, follow these steps:
- On the application’s API permissions page, choose Add a permission.
- Select Microsoft Graph > select Application permissions.
- In the Select Permissions dialog, choose the permissions to configure to the app.
This time I only added Microsoft Graph -> Application -> Files.ReadWrite.All.
PS: Permissions for OneDrive API
Delegated permissions: Also called scopes, allow the application to act on behalf of the signed-in user. (access on behalf of a user)
Permission | Display String | Description | Admin Consent Required |
---|---|---|---|
Files.Read | Read user files | Allows the app to read the signed-in user’s files. | No |
Files.Read.All | Read all files that user can access | Allows the app to read all files the signed-in user can access. | No |
Files.ReadWrite | Have full access to user files | Allows the app to read, create, update, and delete the signed-in user’s files. | No |
Files.ReadWrite.All | Have full access to all files user can access | Allows the app to read, create, update, and delete all files the signed-in user can access. | No |
Files.ReadWrite.AppFolder | Have full access to the application’s folder (preview) | (Preview) Allows the app to read, create, update, and delete files in the application’s folder. | No |
Files.Read.Selected | Read files that the user selects | Limited support in Microsoft Graph – see Remarks (Preview) Allows the app to read files that the user selects. The app has access for several hours after the user selects a file. | No |
Files.ReadWrite.Selected | Read and write files that the user selects | Limited support in Microsoft Graph — see Remarks (Preview) Allows the app to read and write files that the user selects. The app has access for several hours after the user selects a file. | No |
Application permissions: Also called app roles, allow the app to access data on its own, without a signed-in user. (access without a user)
Permission | Display String | Description | Admin Consent Required |
---|---|---|---|
Files.Read.All | Read files in all site collections | Allows the app to read all files in all site collections without a signed in user. | Yes |
Files.ReadWrite.All | Read and write files in all site collections | Allows the app to read, create, update, and delete all files in all site collections without a signed in user. | Yes |
Then we can test in Postman to see if it is accessible.
Enter the following info in Postman. (You can use variables instead to keep sensitive data secure)
Access Token URL: https://login.microsoftonline.com/d8f36038-1f93-4543-affc-5dc92b6ee871/oauth2/v2.0/token (Change it to your tenant ID)
Client ID: a80c03cf-6ffa-4b6e-b2c8-6005310d3d87
Client Secret: fME7Q~cAaSBhXMGZoHY3ei64nn1fxGpqF42mh
Scope: https://graph.microsoft.com/.default (Note that Scope is different from BC)
Cilent Authentication: Send client credentials in body
Test Endpoint:
https://graph.microsoft.com/v1.0/users/{UserName}/drive/root/
For example,
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root
Add children to access the contents of My files in OneDrive.
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children
For example,
Then we can add the folder name to read the folder information. For example, OneDriveAPITest
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children/OneDriveAPITest
Or
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com//drive/root:/OneDriveAPITest
As above, add children to read the contents of the folder.
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children/OneDriveAPITest/children
Or
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest:/children
For example,
Finally, let’s read the contents of the file, Items.csv.
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/{FileName}:/content
For example,
https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/Items.csv:/content
PS:
1. If you get the following error, please check the API permissions in App Registration.
“code”: “AccessDenied”,
“message”: “Either scp or roles claim need to be present in the token.”,
2. If you encounter a 401 Unauthorized error, please check whether the Scope is set to the graph API. (https://graph.microsoft.com/.default)
“code”: “InvalidAuthenticationToken”,
“message”: “Access token validation failure. Invalid audience.”,
3. If you use delegated permissions, use the following endpoint:
https://graph.microsoft.com/v1.0/me/drive/root/
4. When the file cannot be found, the following error message will be displayed:
“code”: “itemNotFound”,
“message”: “The resource could not be found.”
5. When opening the file in Postman, because it can only be opened in XML, HTML, Txt or Json format, the following situation may occur, but the test is successful.
OK, next let’s see if we can get these files from AL. Here are a few important points.
Get OAuth Token:
Using OAuth Token to access OneDrive:
Reading file contents:
This time I did three simple tests in total, I hope it can be of some help to everyone.
1. Read all file names from a specified folder in OneDrive.
It is generally used to test whether OneDrive can be accessed. In addition, I only took the file name, but the json content displayed in Postman can be taken.
Test video:
Source Code: GitHub (Please note that the source code is for reference only, you can improve it according to your own needs)
pageextension 50100 CustomerListExt extends "Customer List"
{
actions
{
addafter("Sent Emails")
{
action(ImportFileFromOneDrive)
{
Caption = 'Import File From OneDrive';
ApplicationArea = All;
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
Image = GetActionMessages;
trigger OnAction()
var
OneDriveHandler: Codeunit OneDriveHandler;
begin
OneDriveHandler.Run();
end;
}
}
}
}
codeunit 50120 OneDriveHandler
{
trigger OnRun()
begin
ImportFilesFromOneDrive();
end;
procedure ImportFilesFromOneDrive()
var
HttpClient: HttpClient;
HttpRequestMessage: HttpRequestMessage;
HttpResponseMessage: HttpResponseMessage;
Headers: HttpHeaders;
JsonResponse: JsonObject;
JsonArray: JsonArray;
JsonToken: JsonToken;
JsonTokenLoop: JsonToken;
JsonValue: JsonValue;
JsonObjectLoop: JsonObject;
AuthToken: SecretText;
OneDriveFolderUrl: Text;
ResponseText: Text;
FileName: Text;
begin
// Get OAuth token
AuthToken := GetOAuthToken();
if AuthToken.IsEmpty() then
Error('Failed to obtain access token.');
// Define the OneDrive folder URL
// delegated permissions
//OneDriveFolderUrl := 'https://graph.microsoft.com/v1.0/me/drive/root/children';
// application permissions (replace with the actual user principal name)
OneDriveFolderUrl := 'https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children/OneDriveAPITest/children';
// Initialize the HTTP request
HttpRequestMessage.SetRequestUri(OneDriveFolderUrl);
HttpRequestMessage.Method := 'GET';
HttpRequestMessage.GetHeaders(Headers);
Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', AuthToken));
// Send the HTTP request
if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
// Log the status code for debugging
//Message('HTTP Status Code: %1', HttpResponseMessage.HttpStatusCode());
if HttpResponseMessage.IsSuccessStatusCode() then begin
HttpResponseMessage.Content.ReadAs(ResponseText);
JsonResponse.ReadFrom(ResponseText);
if JsonResponse.Get('value', JsonToken) then begin
JsonArray := JsonToken.AsArray();
foreach JsonTokenLoop in JsonArray do begin
JsonObjectLoop := JsonTokenLoop.AsObject();
if JsonObjectLoop.Get('name', JsonTokenLoop) then begin
JsonValue := JsonTokenLoop.AsValue();
if FileName = '' then begin
FileName := JsonValue.AsText();
end else begin
FileName := FileName + '\' + JsonValue.AsText();
end;
end;
end;
Message(FileName);
end;
end else begin
//Report errors!
HttpResponseMessage.Content.ReadAs(ResponseText);
Error('Failed to fetch files from OneDrive: %1 %2', HttpResponseMessage.HttpStatusCode(), ResponseText);
end;
end else
Error('Failed to send HTTP request to OneDrive');
end;
procedure GetOAuthToken() AuthToken: SecretText
var
ClientID: Text;
ClientSecret: Text;
TenantID: Text;
AccessTokenURL: Text;
OAuth2: Codeunit OAuth2;
Scopes: List of [Text];
begin
ClientID := 'b4fe1687-f1ab-4bfa-b494-0e2236ed50bd';
ClientSecret := 'huL8Q~edsQZ4pwyxka3f7.WUkoKNcPuqlOXv0bww';
TenantID := '7e47da45-7f7d-448a-bd3d-1f4aa2ec8f62';
AccessTokenURL := 'https://login.microsoftonline.com/' + TenantID + '/oauth2/v2.0/token';
Scopes.Add('https://graph.microsoft.com/.default');
if not OAuth2.AcquireTokenWithClientCredentials(ClientID, ClientSecret, AccessTokenURL, '', Scopes, AuthToken) then
Error('Failed to get access token from response\%1', GetLastErrorText());
end;
}
2. Read all files from the specified folder in OneDrive and add them to the Customer 10000’s Attachments
Test video:
Source Code: GitHub (Please note that the source code is for reference only, you can improve it according to your own needs)
pageextension 50100 CustomerListExt extends "Customer List"
{
actions
{
addafter("Sent Emails")
{
action(ImportFileFromOneDrive)
{
Caption = 'Import File From OneDrive';
ApplicationArea = All;
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
Image = GetActionMessages;
trigger OnAction()
var
OneDriveHandler: Codeunit OneDriveHandler;
begin
OneDriveHandler.Run();
end;
}
}
}
}
codeunit 50120 OneDriveHandler
{
trigger OnRun()
begin
ImportFilesFromOneDrive();
end;
procedure ImportFilesFromOneDrive()
var
HttpClient: HttpClient;
HttpRequestMessage: HttpRequestMessage;
HttpResponseMessage: HttpResponseMessage;
Headers: HttpHeaders;
JsonResponse: JsonObject;
JsonArray: JsonArray;
JsonToken: JsonToken;
JsonTokenLoop: JsonToken;
JsonValue: JsonValue;
JsonObjectLoop: JsonObject;
AuthToken: SecretText;
OneDriveFolderUrl: Text;
ResponseText: Text;
FileName: Text;
FileContent: InStream;
begin
// Get OAuth token
AuthToken := GetOAuthToken();
if AuthToken.IsEmpty() then
Error('Failed to obtain access token.');
// Define the OneDrive folder URL
// delegated permissions
//OneDriveFolderUrl := 'https://graph.microsoft.com/v1.0/me/drive/root/children';
// application permissions (replace with the actual user principal name)
OneDriveFolderUrl := 'https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children/OneDriveAPITest/children';
// Initialize the HTTP request
HttpRequestMessage.SetRequestUri(OneDriveFolderUrl);
HttpRequestMessage.Method := 'GET';
HttpRequestMessage.GetHeaders(Headers);
Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', AuthToken));
// Send the HTTP request
if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
// Log the status code for debugging
//Message('HTTP Status Code: %1', HttpResponseMessage.HttpStatusCode());
if HttpResponseMessage.IsSuccessStatusCode() then begin
HttpResponseMessage.Content.ReadAs(ResponseText);
JsonResponse.ReadFrom(ResponseText);
if JsonResponse.Get('value', JsonToken) then begin
JsonArray := JsonToken.AsArray();
foreach JsonTokenLoop in JsonArray do begin
JsonObjectLoop := JsonTokenLoop.AsObject();
if JsonObjectLoop.Get('name', JsonTokenLoop) then begin
JsonValue := JsonTokenLoop.AsValue();
FileName := JsonValue.AsText();
DownloadAndProcessFile(FileName, AuthToken);
end;
end;
end;
end else begin
//Report errors!
HttpResponseMessage.Content.ReadAs(ResponseText);
Error('Failed to fetch files from OneDrive: %1 %2', HttpResponseMessage.HttpStatusCode(), ResponseText);
end;
end else
Error('Failed to send HTTP request to OneDrive');
end;
procedure GetOAuthToken() AuthToken: SecretText
var
ClientID: Text;
ClientSecret: Text;
TenantID: Text;
AccessTokenURL: Text;
OAuth2: Codeunit OAuth2;
Scopes: List of [Text];
begin
ClientID := 'b4fe1687-f1ab-4bfa-b494-0e2236ed50bd';
ClientSecret := 'huL8Q~edsQZ4pwyxka3f7.WUkoKNcPuqlOXv0bww';
TenantID := '7e47da45-7f7d-448a-bd3d-1f4aa2ec8f62';
AccessTokenURL := 'https://login.microsoftonline.com/' + TenantID + '/oauth2/v2.0/token';
Scopes.Add('https://graph.microsoft.com/.default');
if not OAuth2.AcquireTokenWithClientCredentials(ClientID, ClientSecret, AccessTokenURL, '', Scopes, AuthToken) then
Error('Failed to get access token from response\%1', GetLastErrorText());
end;
procedure DownloadAndProcessFile(FileName: Text; AuthToken: SecretText)
var
HttpClient: HttpClient;
HttpRequestMessage: HttpRequestMessage;
HttpResponseMessage: HttpResponseMessage;
Headers: HttpHeaders;
FileUrl: Text;
FileContent: InStream;
begin
// Define the file URL
FileUrl := 'https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/' + FileName + ':/content';
// Initialize the HTTP request
HttpRequestMessage.SetRequestUri(FileUrl);
HttpRequestMessage.Method := 'GET';
HttpRequestMessage.GetHeaders(Headers);
Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', AuthToken));
// Send the HTTP request
if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
if HttpResponseMessage.IsSuccessStatusCode() then begin
HttpResponseMessage.Content.ReadAs(FileContent);
// Process the file content (e.g., import into a table)
ImportFileContent(FileName, FileContent);
end else
Error('Failed to download file: %1', HttpResponseMessage.HttpStatusCode());
end else
Error('Failed to send HTTP request to download file');
end;
procedure ImportFileContent(FileName: Text; FileContent: InStream)
var
Cust: Record Customer;
DocAttach: Record "Document Attachment";
FileExtension: Text;
FileMgt: Codeunit "File Management";
begin
if Cust.Get(10000) then begin
FileExtension := CopyStr(FileMgt.GetExtension(FileName), 1, MaxStrLen(FileExtension));
FileName := CopyStr(FileMgt.GetFileNameWithoutExtension(FileName), 1, MaxStrLen(FileName));
DocAttach.Init();
DocAttach.Validate("Table ID", Database::Customer);
DocAttach.Validate("No.", Cust."No.");
DocAttach.Validate("File Name", FileName);
DocAttach.Validate("File Extension", FileExtension);
DocAttach."Document Reference ID".ImportStream(FileContent, FileName);
DocAttach.Insert(true);
end;
end;
}
3. Read the specified csv file from the specified folder of OneDrive and import the data into BC
Since this only requires reading one file, it is relatively simple. (However, if you only know the folder and don’t know the file name, or want to import data from multiple files, continue to use the loop in the above method)
Test video:
Source Code: GitHub (Please note that the source code is for reference only, you can improve it according to your own needs)
pageextension 50100 ItemListExt extends "Item List"
{
actions
{
addafter(AdjustInventory)
{
action(ImportFileFromOneDrive)
{
Caption = 'Import File From OneDrive';
ApplicationArea = All;
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
Image = GetActionMessages;
trigger OnAction()
var
OneDriveHandler: Codeunit OneDriveHandler;
begin
OneDriveHandler.Run();
end;
}
}
}
}
codeunit 50120 OneDriveHandler
{
trigger OnRun()
begin
ImportFilesFromOneDrive();
end;
var
CSVBuffer: Record "CSV Buffer" temporary;
procedure ImportFilesFromOneDrive()
var
HttpClient: HttpClient;
HttpRequestMessage: HttpRequestMessage;
HttpResponseMessage: HttpResponseMessage;
Headers: HttpHeaders;
JsonResponse: JsonObject;
JsonArray: JsonArray;
JsonToken: JsonToken;
JsonTokenLoop: JsonToken;
JsonValue: JsonValue;
JsonObjectLoop: JsonObject;
AuthToken: SecretText;
OneDriveFolderUrl: Text;
ResponseText: Text;
FileName: Text;
FileContent: InStream;
FileUrl: Text;
begin
// Get OAuth token
AuthToken := GetOAuthToken();
if AuthToken.IsEmpty() then
Error('Failed to obtain access token.');
// Define the file name
FileName := 'ImportItems.csv';
// Define the file URL
FileUrl := 'https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/' + FileName + ':/content';
// Initialize the HTTP request
HttpRequestMessage.SetRequestUri(FileUrl);
HttpRequestMessage.Method := 'GET';
HttpRequestMessage.GetHeaders(Headers);
Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', AuthToken));
// Send the HTTP request
if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
if HttpResponseMessage.IsSuccessStatusCode() then begin
HttpResponseMessage.Content.ReadAs(FileContent);
// Process the file content (e.g., import into a table)
ImportFileContent(FileContent);
end else
Error('Failed to download file: %1', HttpResponseMessage.HttpStatusCode());
end else
Error('Failed to send HTTP request to download file');
end;
procedure GetOAuthToken() AuthToken: SecretText
var
ClientID: Text;
ClientSecret: Text;
TenantID: Text;
AccessTokenURL: Text;
OAuth2: Codeunit OAuth2;
Scopes: List of [Text];
begin
ClientID := 'b4fe1687-f1ab-4bfa-b494-0e2236ed50bd';
ClientSecret := 'huL8Q~edsQZ4pwyxka3f7.WUkoKNcPuqlOXv0bww';
TenantID := '7e47da45-7f7d-448a-bd3d-1f4aa2ec8f62';
AccessTokenURL := 'https://login.microsoftonline.com/' + TenantID + '/oauth2/v2.0/token';
Scopes.Add('https://graph.microsoft.com/.default');
if not OAuth2.AcquireTokenWithClientCredentials(ClientID, ClientSecret, AccessTokenURL, '', Scopes, AuthToken) then
Error('Failed to get access token from response\%1', GetLastErrorText());
end;
procedure ImportFileContent(FileContent: InStream)
var
Item: Record Item;
LineNo: Integer;
begin
CSVBuffer.Reset();
CSVBuffer.DeleteAll();
CSVBuffer.LoadDataFromStream(FileContent, ',');
for LineNo := 2 to CSVBuffer.GetNumberOfLines() do begin
Item.Init();
Item.Validate("No.", GetValueAtCell(LineNo, 1));
Item.Insert(true);
Item.Validate(Description, GetValueAtCell(LineNo, 2));
case GetValueAtCell(LineNo, 3) of
'Inventory':
Item.Validate(Type, Item.Type::"Inventory");
'Service':
Item.Validate(Type, Item.Type::"Service");
'Non-Inventory':
Item.Validate(Type, Item.Type::"Non-Inventory");
end;
Evaluate(Item.GTIN, GetValueAtCell(LineNo, 4));
Evaluate(Item.GTIN, GetValueAtCell(LineNo, 5));
Item.Validate("Base Unit of Measure", GetValueAtCell(LineNo, 6));
Item.Modify(true);
end;
end;
local procedure GetValueAtCell(RowNo: Integer; ColNo: Integer): Text
begin
if CSVBuffer.Get(RowNo, ColNo) then
exit(CSVBuffer.Value)
else
exit('');
end;
}
PS: Dynamics 365 Business Central: How to import data from CSV (comma-separated values) file
At this point, I believe you should be able to understand how to read data from OneDrive. In fact, not only that, you can add the file name or URL as a setting so that users can modify it, which is more flexible. Alternatively, you can set these process in the Job Queue so that the system can run automatically. Give it a try!!!😁
PS: The comments in some codes refer to the template in the question below, you can also refer to it directly.
Solved: User’s mysite not found. OneDrive to Business Central
END
Hope this will help.
Thanks.
ZHU
コメント