使用 Microsoft Graph 将 C# Asp.Net Core 中的文件上传到 Sharepoint/OneDrive,无需用户交互

Upload a file in C# Asp.Net Core to Sharepoint/OneDrive using Microsoft Graph without user interaction

当我尝试使用 Microsoft Graph API 通过守护程序应用程序将文件上传到我的 OneDrive 时,我收到错误 400 Bad Request。我使用 HttpClient,而不是 GraphServiceClient,因为后者假定交互并与 DelegatedAuthenticationProvider(?) 一起工作。

主要方法 Upload 通过 Helper AuthenticationConfig 获取 AccessToken 并使用 Helper ProtectedApiCallHelper.

将文件放入 OneDrive/SharePoint
[HttpPost]
    public async Task<IActionResult> Upload(IFormFile file)
    {            
        var toegang = new AuthenticationConfig();
        var token = toegang.GetAccessTokenAsync().GetAwaiter().GetResult();

        var httpClient = new HttpClient();
        string bestandsnaam = file.FileName;
        var serviceEndPoint = "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/";            

        var wurl = serviceEndPoint + bestandsnaam + "/content";
// The variable wurl looks as follows: "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content"
        var apicaller = new ProtectedApiCallHelper(httpClient);
        apicaller.PostWebApi(wurl, token.AccessToken, file).GetAwaiter();

        return View();
    }

我使用以下标准助手获得了正确的访问令牌 AuthenticationConfig.GetAccessToken()

public async Task<AuthenticationResult> GetAccessTokenAsync()
    {
        AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");            
        IConfidentialClientApplication app;

        app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
            .WithClientSecret(config.ClientSecret)
            .WithAuthority(new Uri(config.Authority))
            .Build();

        string[] scopes = new string[] { "https://graph.microsoft.com/.default" };

        AuthenticationResult result = null;
        try
        {
            result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
            return result;
        }
        catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011"))
        {
            ...
            return result;
        }
    }

使用 AccessToken、Graph-Url 和要上传的文件(作为 IFormFile),Helper ProtectedApiCallHelper.PostWebApi 被调用

public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
    {
        Stream stream = fileToUpload.OpenReadStream();
        var x = stream.Length;
        HttpContent content = new StreamContent(stream);

        if (!string.IsNullOrEmpty(accessToken))
        {
            var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;               
            HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));             
            defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

// Here the 400 Bad Request happens
            HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);

            if (response.IsSuccessStatusCode)
            {
                return;
            }
            else
            {
                 //error handling                   
                return;
            }
        }
    }

编辑

请参阅下面的工作解决方案。

您可以使用 GraphServiceClient,而无需使用客户端 ID 和客户端密码进行用户交互。首先,创建一个名为 GraphAuthProvider 的 class:

    public class GraphAuthProvider
{
    public async Task<GraphServiceClient> AuthenticateViaAppIdAndSecret(
        string tenantId,
        string clientId, 
        string clientSecret)
    {
        var scopes = new string[] { "https://graph.microsoft.com/.default" };

        // Configure the MSAL client as a confidential client
        var confidentialClient = ConfidentialClientApplicationBuilder
            .Create(clientId)
            .WithAuthority($"https://login.microsoftonline.com/{tenantId}/v2.0")
            .WithClientSecret(clientSecret)
            .Build();

        // Build the Microsoft Graph client. As the authentication provider, set an async lambda
        // which uses the MSAL client to obtain an app-only access token to Microsoft Graph,
        // and inserts this access token in the Authorization header of each API request. 
        GraphServiceClient graphServiceClient =
            new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
            {

                // Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
                var authResult = await confidentialClient
                    .AcquireTokenForClient(scopes)
                    .ExecuteAsync();

                // Add the access token in the Authorization header of the API request.
                requestMessage.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
            })
        );

        return graphServiceClient;
    }
}

然后您可以创建经过身份验证的 GraphServiceClient 并使用它们上传文件,例如上传到 SharePoint:

        GraphServiceClient _graphServiceClient = await _graphAuthProvider.AuthenticateViaAppIdAndSecret(
            tenantId,
            clientId,
            appSecret);


        using (Stream fileStream = new FileStream(
            fileLocation,
            FileMode.Open,
            FileAccess.Read))
        {
            resultDriveItem = await _graphServiceClient.Sites[sites[0]]
                    .Drives[driveId].Root.ItemWithPath(fileName).Content.Request().PutAsync<DriveItem>(fileStream);

       }

关于权限:您可能需要比 Files.ReadWrite.All 更多的权限。据我所知,应用程序需要应用程序权限 Sites.ReadWrite.All 才能将文档上传到 SharePoint。

根据文档:Upload or replace the contents of a DriveItem

如果使用客户端凭证流(没有用户的 M2M 流),您应该使用以下请求:

PUT /drives/{drive-id}/items/{parent-id}:/{filename}:/content

而不是:

https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content

这是使用 GraphServiceClient 的最终工作示例

public async Task<DriveItem> UploadSmallFile(IFormFile file, bool uploadToSharePoint)
    {
        IFormFile fileToUpload = file;
        Stream ms = new MemoryStream();

        using (ms = new MemoryStream()) //this keeps the stream open
        {
            await fileToUpload.CopyToAsync(ms);
            ms.Seek(0, SeekOrigin.Begin);
            var buf2 = new byte[ms.Length];
            ms.Read(buf2, 0, buf2.Length);

            ms.Position = 0; // Very important!! to set the position at the beginning of the stream
            GraphServiceClient _graphServiceClient = await AuthenticateViaAppIdAndSecret();

            DriveItem uploadedFile = null;
            if (uploadToSharePoint == true)
            {
                uploadedFile = (_graphServiceClient
                .Sites["root"]
                .Drives["{DriveId}"]
                .Items["{Id_of_Targetfolder}"]
                .ItemWithPath(fileToUpload.FileName)
                .Content.Request()
                .PutAsync<DriveItem>(ms)).Result;
            }
            else
            {
                // Upload to OneDrive (for Business)
                uploadedFile = await _graphServiceClient
                .Users["{Your_EmailAdress}"]
                .Drive
                .Root
                .ItemWithPath(fileToUpload.FileName)
                .Content.Request()
                .PutAsync<DriveItem>(ms);
            }

            ms.Dispose(); //clears memory
            return uploadedFile; //returns a DriveItem. 
        }
    }

您也可以使用 HttpClient

public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
    {

        //Create a Stream and convert it to a required HttpContent-stream (StreamContent).
        // Important is the using{...}. This keeps the stream open until processed
        using (MemoryStream data = new MemoryStream())
        {
            await fileToUpload.CopyToAsync(data);
            data.Seek(0, SeekOrigin.Begin);
            var buf = new byte[data.Length];
            data.Read(buf, 0, buf.Length);
            data.Position = 0;
            HttpContent content = new StreamContent(data);


            if (!string.IsNullOrEmpty(accessToken))
            {
                // NO Headers other than the AccessToken should be added. If you do
                // an Error 406 is returned (cannot process). So, no Content-Types, no Conentent-Dispositions

                var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;                    
                defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);

                if (response.IsSuccessStatusCode)
                {
                    return;
                }
                else
                {
                    // do something else
                    return;
                }
            }
            content.Dispose();
            data.Dispose();
        } //einde using memorystream 
    }
}