Skip to main content

Implementing SharePoint File CRUD Operations using Microsoft Graph API

Implementing SharePoint File CRUD Operations using Microsoft Graph API
Table of Contents

Introduction
#

Storing files in SharePoint from a .NET service sounds like it should be a solved problem, and it is, as long as you make one decision early: delegated or application permissions. That single choice changes who shows up as “Created by” on every file and whether your service needs a signed-in user at all. Get it wrong and you rebuild the auth layer later.

This post implements the full set of file operations (create, read, update, delete) against a SharePoint document library through the Microsoft Graph API. It covers registering the app in Microsoft Entra ID, authenticating with a client secret, caching the site and drive IDs so you are not resolving them on every call, and exposing the operations through an ASP.NET Core controller.

What’s in Microsoft Graph?
#

Microsoft Graph is a single REST endpoint over most of the Microsoft cloud: Microsoft 365 (SharePoint, OneDrive, Outlook, Teams, and more), Entra ID, Windows, Dynamics 365, and Intune. The payoff for this post is that the authentication and request patterns you learn for SharePoint carry over to every other service behind that endpoint.

Microsoft Graph API Ecosystem

What You’ll Learn
#

You’ll learn how to:

  • Register an application in Microsoft Entra ID (formerly Azure Active Directory)
  • Authenticate to the Microsoft Graph API with a client secret
  • Navigate the SharePoint site and drive structure
  • Implement file operations: upload, download, update, delete
  • Cache site and drive IDs to cut redundant API calls

Aimed at developers new to Microsoft Graph as well as those who just want the implementation pattern.

Key Concepts for Beginners
#

Before diving into the implementation, let’s understand some fundamental concepts:

  1. Microsoft Graph API: A unified REST API endpoint that provides access to Microsoft 365 cloud services like SharePoint, OneDrive, Outlook, and more.

  2. SharePoint Document Libraries: Collections in SharePoint sites where you can store, organize, and share files.

  3. OAuth Authentication: The authentication protocol used by Microsoft Graph API to securely access resources.

  4. Delegated vs. Application Permissions: Different permission models that determine how your application accesses Microsoft 365 resources.

Prerequisites
#

Before you begin, you’ll need:

  • A Microsoft 365 account with access to SharePoint Online.
  • A registered application in Microsoft Entra ID (formerly Azure Active Directory) with appropriate permissions.
  • Basic understanding of .NET and C# programming.
  • Visual Studio or Visual Studio Code with .NET SDK installed.

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 you add Microsoft Graph permissions during app registration, you pick one of two models. This choice decides whose identity performs the operations, so make it before you write any code.

Delegated permissionsApplication permissions
Runs asA signed-in user (user’s token)The app itself (no user)
“Created by” / “Modified by”The logged-in userThe application’s identity
Best forActions on a user’s behalf, audit trails tied to that userBackground services, scheduled jobs, automation

This post uses application permissions (Sites.ReadWrite.All), because the file operations run from a backend service with no user in the loop.

Architecture and Code Flow
#

The application follows a layered architecture pattern with clear separation of concerns. Below is an architectural diagram of the codebase:

flowchart TB Client[Client Application] SpoWebApi[Spo.WebApi] SpoGraphApi[Spo.GraphApi Library] GraphApiClient[GraphApiClient] GraphApiFactory[GraphApiClientFactory] AuthHandler[GraphApiAuthenticationHandler] GraphAPI[Microsoft Graph API] SharePoint[SharePoint Online] Client -->|HTTP Requests| SpoWebApi SpoWebApi -->|Uses| SpoGraphApi SpoGraphApi -->|Creates| GraphApiFactory GraphApiFactory -->|Creates| GraphApiClient GraphApiClient -->|Authenticated Requests| GraphAPI GraphApiClient -->|Uses| AuthHandler AuthHandler -->|OAuth 2.0| GraphAPI GraphAPI -->|Interacts with| SharePoint subgraph "Client Layer" Client end subgraph "API Layer" SpoWebApi end subgraph "Core Library" SpoGraphApi GraphApiFactory GraphApiClient AuthHandler end subgraph "External Services" GraphAPI SharePoint end

Component Descriptions
#

  1. Client Application: External applications that consume the API endpoints.

  2. Spo.WebApi: ASP.NET Core Web API that exposes endpoints for SharePoint file operations. It acts as a façade over the core library.

  3. Spo.GraphApi Library: Core library containing all the logic for interacting with Microsoft Graph API.

  4. GraphApiClientFactory: Factory class responsible for creating instances of GraphApiClient with proper authentication.

  5. GraphApiClient: Main class that handles all file operations by making authenticated requests to Microsoft Graph API.

  6. GraphApiAuthenticationHandler: Handles OAuth 2.0 authentication with Azure AD and token management.

  7. Microsoft Graph API: Microsoft’s unified API endpoint that provides access to SharePoint data.

  8. SharePoint Online: The destination service where files are stored and managed.

Request Flow
#

  1. The client sends an HTTP request to one of the endpoints in Spo.WebApi.
  2. The controller in Spo.WebApi processes the request and calls the appropriate method in the Spo.GraphApi library.
  3. The GraphApiClientFactory creates an authenticated instance of GraphApiClient.
  4. GraphApiClient sends authenticated requests to Microsoft Graph API.
  5. Microsoft Graph API interacts with SharePoint Online to perform the requested operation.
  6. The response follows the same path back to the client.

Implementation Steps
#

Following the architectural flow, here’s how we’ll implement each component of the system:

Step 1: Application Registration in Azure AD
#

  1. Log into the Azure Portal: Navigate to Microsoft Entra ID in the Azure portal.

  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.

  • Attaches a valid access token to every outgoing HTTP request.

  • 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 attaches the access token to each HTTP request.

  • 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:

  • Resolving Site ID and Drive ID at runtime keeps the client flexible across sites and drives.

  • Caching them removes redundant API calls and speeds up later requests.

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 });
    }
}

Key takeaways
#

  • Pick the permission model first. Delegated ties every change to a signed-in user; application permissions run headless and stamp the app as the author. Switching later means reworking auth.
  • Cache the site and drive IDs. Resolving them on every request is a wasted round trip; a day-long IDistributedCache entry removes it.
  • Cache the token too. The handler stores it for 50 minutes, comfortably inside its lifetime, so most requests skip the token call.
  • Let conflictBehavior=rename handle name clashes on add, so an upload never fails just because the name is already taken.

Start from the reference implementation and swap in your tenant, site, and drive names: sharepoint-graph-api on GitHub. For the API surface itself, the Microsoft Graph SharePoint docs are the reference.

Related

Implementing Activity Logging with Custom Attributes

··6 mins
The first version of audit logging in the Contact Management Application was what most codebases end up with: _logger.LogInformation("Creating contact...") copy-pasted into every controller action. It worked, until someone forgot one, and the one they forgot was of course the action the auditors asked about. Logging that depends on developer discipline isn’t an audit trail; it’s a suggestion.

Unit of Work Pattern and Its Role in Managing Transactions

··6 mins
The bug that makes people care about the Unit of Work pattern is the partial write. An operation inserts a row in one table, fails on the second, and the database lands in a state nobody designed for. I’ve spent enough late evenings reconstructing “how did this record end up half-created” timelines to want transaction boundaries stated explicitly in code, not implied by hope.