Implementing SharePoint Online File CRUD Operations Using Microsoft Graph API

Implementing SharePoint File CRUD Operations Using Microsoft Graph API
Implementing SharePoint File CRUD Operations Using Microsoft Graph API

Integrating SharePoint Online file management into your .NET applications can significantly streamline collaboration and document handling within your organization. By leveraging the Microsoft Graph API, you can implement Create, Read, Update, and Delete (CRUD) operations on SharePoint Online files programmatically. This comprehensive guide walks you through the entire process—from application registration in Microsoft Entra ID (formerly Azure Active Directory) to caching and controller implementation—ensuring secure, efficient, and scalable file operations.

Prerequisites

  • A Microsoft 365 account with access to SharePoint Online.
  • A registered application in Microsoft Entra ID (formerly Azure Active Directory) with appropriate permissions.
  • Visual Studio Code or a compatible IDE.

What is Application Registration?

Application registration in Microsoft Entra ID (formerly Azure Active Directory) defines the identity and permissions of your application. By registering your app, you can securely access Microsoft 365 services such as SharePoint through the Microsoft Graph API.

Delegated vs. Application Permissions
When adding Microsoft Graph permissions during app registration, you have two primary choices:

  1. Delegated Permissions:
    These permissions run in the context of a signed-in user. The API calls made by your application will use the user’s access token. Consequently:
    • “Created by” and “Modified by” in SharePoint: These fields will reflect the currently logged-in user making the request.
    • Use Case: Ideal when you want actions to be taken explicitly on behalf of a user, preserving user identity and audit trails.
  2. Application Permissions:
    These permissions do not require a user to sign in. Your application runs as a background service or daemon:
    • “Created by” and “Modified by” in SharePoint: These fields will show the application’s identity rather than a user’s name.
    • Use Case: Suitable for backend services, scheduled tasks, or automation scenarios where no user interaction is required.


Step 1: Application Registration in Azure AD

  1. Log into the Azure Portal: Navigate to Azure Active Directory.
  2. Register a New Application:
    • Go to App registrations > New registration.
    • Enter a name for your application.
    • Click Register.
  3. API Permissions
    • Navigate to API Permissions > Add a permission > Microsoft Graph.
    • Select Delegated permissions or Application permissions based on your use case. (We are selecting Application Permission):
      • Sites.ReadWrite.All
    • Click Grant admin consent.
  4. Create a Client Secret:
    • Go to Certificates & secrets > New client secret.
    • Copy the secret value. It will be required for authentication.

Take Note of App Details:

  • Record the Application (client) ID, Directory (tenant) ID, and the client secret for configuration.

Step 2: Configure the Application

Add Configuration Settings

In your .NET project, add a configuration section in appsettings.json:

{
  "GraphApiOptions": {
    "BaseGraphUri": "https://graph.microsoft.com/v1.0",
    "BaseSpoSiteUri": "yourdomain.sharepoint.com",
    "TenantId": "<your-tenant-id>",
    "ClientId": "<your-client-id>",
    "SecretId": "<your-client-secret>",
    "Scope": "https://graph.microsoft.com/.default"
  }
}

Step 3: Implement Authentication Handler

Why Do We Need Authentication?

Microsoft Graph API requires an OAuth 2.0 token for secure access. The token authenticates API requests and defines the permissions granted to the application.

Token Caching
Fetching new tokens for every request is inefficient. To improve performance, cache the access token using IDistributedCache.

Key Points of the GraphApiAuthenticationHandler:

  • Caches tokens to reduce repeated calls.
  • Ensures that each HTTP request includes a valid access token.
  • Uses ClientSecretCredential to retrieve tokens.
internal class GraphApiAuthenticationHandler : DelegatingHandler
{
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger<GraphApiAuthenticationHandler> _logger;
    private readonly GraphApiOptions _graphApiOptions;

    public GraphApiAuthenticationHandler(
        IDistributedCache distributedCache,
        ILogger<GraphApiAuthenticationHandler> logger,
        IOptions<GraphApiOptions> options)
    {
        _distributedCache = distributedCache;
        _logger = logger;
        _graphApiOptions = options.Value;
        InnerHandler = new HttpClientHandler();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string accessToken = await GetAccessTokenAsync(cancellationToken);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        return await base.SendAsync(request, cancellationToken);
    }

    private async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
    {
        var cachedToken = await _distributedCache.GetAsync("GraphApiToken", cancellationToken);
        if (cachedToken?.Length > 0)
            return Encoding.UTF8.GetString(cachedToken);

        var clientCredential = new ClientSecretCredential(
            _graphApiOptions.TenantId,
            _graphApiOptions.ClientId,
            _graphApiOptions.SecretId);

        var tokenRequestContext = new TokenRequestContext(new[] { _graphApiOptions.Scope });
        var tokenResponse = await clientCredential.GetTokenAsync(tokenRequestContext, cancellationToken);

        var cacheEntryOptions = new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(50));
        await _distributedCache.SetAsync("GraphApiToken", Encoding.UTF8.GetBytes(tokenResponse.Token), cacheEntryOptions, cancellationToken);

        return tokenResponse.Token;
    }
}

Key Points:

  • DelegatingHandler ensures each HTTP request includes the access token.
  • IDistributedCache stores the token to minimize API calls for fetching new tokens.

Step 4: Implement the Graph API Client

What is the Graph API Client?
The GraphApiClient provides a clean abstraction over the raw HTTP calls. It handles fetching and caching the Site ID and Drive ID, as well as executing file-related operations (Get, Add, Update, Delete, etc.).

Benefits of Fetching and Caching IDs:

  • Dynamic retrieval of Site ID and Drive ID ensures flexibility.
  • Caching reduces redundant API calls and improves application performance.

Example Methods:

  • File Operations (CRUD): Implemented using GetAsync, UploadAsync, and DeleteAsync helper methods.
  • GetSiteId: Retrieves and caches the Site ID.
  • GetDrive: Retrieves and caches the Drive ID for a given site and drive.
internal class GraphApiCient : IGraphApiCient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<GraphApiCient> _logger;
    private readonly GraphApiOptions _graphApiOptions;
    private readonly IDistributedCache _distributedCache;

    public GraphApiCient(HttpClient httpClient, GraphApiOptions graphApiOptions, ILogger<GraphApiCient> logger, IDistributedCache distributedCache)
    {
        _httpClient = httpClient;
        _logger = logger;
        _graphApiOptions = graphApiOptions;
        _distributedCache = distributedCache;
    }
}

Caching Site ID

private async Task<SiteDetails> GetSiteId(string siteName, CancellationToken cancellationToken = default)
{
    var siteIdByteArray = await _distributedCache.GetAsync(siteName, cancellationToken);
    if (siteIdByteArray?.Length > 0)
    {
        return JsonSerializer.Deserialize<SiteDetails>(Encoding.UTF8.GetString(siteIdByteArray));
    }

    var siteDetails = await GetAsync<SiteDetails>($"/sites/{_graphApiOptions.BaseSpoSiteUri}:/sites/{siteName}");

    DistributedCacheEntryOptions cacheEntryOptions = new DistributedCacheEntryOptions().SetAbsoluteExpiration(new TimeSpan(1, 0, 0, 0));
    _distributedCache.Set(siteName, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(siteDetails)), cacheEntryOptions);

    return siteDetails;
}

Caching Drive ID

private async Task<Drive?> GetDrive(string siteName, string driveName, CancellationToken cancellationToken = default)
{
    var driveDetailsByteArray = await _distributedCache.GetAsync(siteName + driveName, cancellationToken);
    if (driveDetailsByteArray?.Length > 0)
    {
        return JsonSerializer.Deserialize<Drive>(Encoding.UTF8.GetString(driveDetailsByteArray));
    }

    var siteDetails = await GetSiteId(siteName);
    var drives = (await GetAsync<DriveDetails>($"/sites/{siteDetails.Id}/drives?$select=id,name,description,webUrl")).Value;
    var driveDetail = drives?.FirstOrDefault(x => x.Name == driveName);

    DistributedCacheEntryOptions cacheEntryOptions = new DistributedCacheEntryOptions().SetAbsoluteExpiration(new TimeSpan(1, 0, 0, 0));
    _distributedCache.Set(siteName + driveName, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(driveDetail)), cacheEntryOptions);

    return driveDetail;
}

Fetching All Files

public async Task<List<FileDetails>> GetAllFiles(string siteName, string driveName, string path, CancellationToken cancellationToken = default)
{
    var driveDetails = await GetDrive(siteName, driveName, cancellationToken);

    if (driveDetails == null)
        throw new InvalidOperationException($"Drive '{driveName}' not found in site '{siteName}'.");

    var endpoint = $"drives/{driveDetails.Id}/items/root:/{path}:/children?$select=id,name,size,webUrl";
    return (await GetAsync<FileDetailsResponse>(endpoint, cancellationToken)).Value;
}

Adding a File

public async Task<FileDetails> AddFile(string siteName, string driveName, string path, CustomFile file, CancellationToken cancellationToken = default)
{
    var driveDetails = await GetDrive(siteName, driveName, cancellationToken);

    if (driveDetails == null)
        throw new InvalidOperationException($"Drive '{driveName}' not found in site '{siteName}'.");

    await using var memoryStream = new MemoryStream();
    await file.File.CopyToAsync(memoryStream, cancellationToken);

    var endpoint = $"drives/{driveDetails.Id}/items/root:/{path}/{file.Name}:/content?@microsoft.graph.conflictBehavior=rename";
    return await UploadAsync<FileDetails>(endpoint, memoryStream.ToArray(), cancellationToken);
}

Updating a File

public async Task<FileDetails> UpdateFile(string siteName, string driveName, string path, CustomFile file, CancellationToken cancellationToken = default)
{
    var driveDetails = await GetDrive(siteName, driveName, cancellationToken);

    if (driveDetails == null)
        throw new InvalidOperationException($"Drive '{driveName}' not found in site '{siteName}'.");

    await using var memoryStream = new MemoryStream();
    await file.File.CopyToAsync(memoryStream, cancellationToken);

    var endpoint = $"drives/{driveDetails.Id}/items/root:/{path}/{file.Name}:/content";
    return await UploadAsync<FileDetails>(endpoint, memoryStream.ToArray(), cancellationToken);
}

Deleting a File

public async Task DeleteFile(string siteName, string driveName, string path, string fileName, CancellationToken cancellationToken = default)
{
    var driveDetails = await GetDrive(siteName, driveName, cancellationToken);

    if (driveDetails == null)
        throw new InvalidOperationException($"Drive '{driveName}' not found in site '{siteName}'.");

    var endpoint = $"drives/{driveDetails.Id}/items/root:/{path}/{fileName}";
    await DeleteAsync<object>(endpoint, cancellationToken);
}

Reading a File

public async Task<FileDetails> ReadFile(string siteName, string driveName, string path, string fileName, CancellationToken cancellationToken = default)
{
    var driveDetails = await GetDrive(siteName, driveName, cancellationToken);

    if (driveDetails == null)
        throw new InvalidOperationException($"Drive '{driveName}' not found in site '{siteName}'.");

    var endpoint = $"drives/{driveDetails.Id}/items/root:/{path}/{fileName}?$select=id,name,size,webUrl";
    return await GetAsync<FileDetails>(endpoint, cancellationToken);
}

Step 5: Build the Controller

Create an ApiController to expose endpoints for CRUD operations. The following controller methods cover all CRUD functionalities:

Adding a File

[HttpPost("{siteName}/{driveName}/{path}")]
public async Task<IActionResult> AddFile(string siteName, string driveName, string path, [FromForm] CustomFile file, CancellationToken cancellationToken)
{
    try
    {
        var addedFile = await _graphApiClient.AddFile(siteName, driveName, path, file, cancellationToken);
        return CreatedAtAction(nameof(ReadFile), new { siteName, driveName, path, fileName = addedFile.Name }, addedFile);
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = ex.Message });
    }
}

Fetching All Files

[HttpGet("{siteName}/{driveName}/{path}")]
public async Task<IActionResult> GetAllFiles(string siteName, string driveName, string path, CancellationToken cancellationToken)
{
    try
    {
        var files = await _graphApiClient.GetAllFiles(siteName, driveName, path, cancellationToken);
        return Ok(files);
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = ex.Message });
    }
}

Reading a File

[HttpGet("{siteName}/{driveName}/{path}/{fileName}")]
public async Task<IActionResult> ReadFile(string siteName, string driveName, string path, string fileName, CancellationToken cancellationToken)
{
    try
    {
        var file = await _graphApiClient.ReadFile(siteName, driveName, path, fileName, cancellationToken);
        return Ok(file);
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = ex.Message });
    }
}

Updating a File

[HttpPut("{siteName}/{driveName}/{path}")]
public async Task<IActionResult> UpdateFile(string siteName, string driveName, string path, [FromForm] CustomFile file, CancellationToken cancellationToken)
{
    try
    {
        var updatedFile = await _graphApiClient.UpdateFile(siteName, driveName, path, file, cancellationToken);
        return Ok(updatedFile);
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = ex.Message });
    }
}

Deleting a File

[HttpDelete("{siteName}/{driveName}/{path}/{fileName}")]
public async Task<IActionResult> DeleteFile(string siteName, string driveName, string path, string fileName, CancellationToken cancellationToken)
{
    try
    {
        await _graphApiClient.DeleteFile(siteName, driveName, path, fileName, cancellationToken);
        return NoContent();
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = ex.Message });
    }
}

Updating File Metadata

[HttpPatch("{siteName}/{driveName}/{path}/{fileName}")]
public async Task<IActionResult> UpdateFileMetadata(string siteName, string driveName, string path, string fileName, [FromBody] Dictionary<string, object> metadataUpdates, CancellationToken cancellationToken)
{
    try
    {
        if (metadataUpdates == null || metadataUpdates.Count == 0)
            return BadRequest(new { Message = "Metadata updates cannot be null or empty." });

        var updatedFile = await _graphApiClient.UpdateFileMetadata(siteName, driveName, path, fileName, metadataUpdates, cancellationToken);
        return Ok(updatedFile);
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = ex.Message });
    }
}

Conclusion

This implementation provides an end-to-end solution for managing files in SharePoint using the Microsoft Graph API. With authentication, caching, and robust client methods, it ensures efficiency and scalability in handling file operations. By following this guide, you can seamlessly integrate SharePoint file management into your .NET applications.

For a complete reference implementation, visit the GitHub Repository.

For additional details, consult the Microsoft Graph API Documentation.


Get Involved!

  • Try the Code: Test the examples provided and share your results in the comments.
  • Follow Us: Stay updated on new developments by following the project on. GitHub.
  • Subscribe: Sign up for our newsletter to receive expert Azure development tips.
  • Join the Conversation: What challenges have you faced with Microsoft Graph API? Share your experiences in the comments below!


Discover more from Nitin Singh

Subscribe to get the latest posts sent to your email.

Leave a Reply