Skip to main content
  1. Blog/

Implementing SharePoint File CRUD Operations using Microsoft Graph API

·11 mins
Nitin Kumar Singh
Author
Nitin Kumar Singh
I build enterprise AI solutions and cloud-native systems. I write about architecture patterns, AI agents, Azure, and modern development practices — with full source code.
Table of Contents

Introduction
#

Microsoft Graph API provides a unified endpoint for accessing Microsoft 365 services, including SharePoint. This article demonstrates how to implement create, read, update, and delete (CRUD) operations for files in SharePoint document libraries using Microsoft Graph API and .NET. You’ll learn how to authenticate to Microsoft Graph, navigate SharePoint site structure, and manage files programmatically.

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.

What’s in Microsoft Graph?
#

Microsoft Graph exposes REST APIs and client libraries to access data across a wide range of Microsoft cloud services:

  • Microsoft 365 core services: Bookings, Calendar, Delve, Excel, Microsoft Purview eDiscovery, Microsoft Search, OneDrive, OneNote, Outlook/Exchange, People (Outlook contacts), Planner, SharePoint, Teams, To Do, Viva Insights
  • Enterprise Mobility + Security services: Advanced Threat Analytics, Advanced Threat Protection, Microsoft Entra, Identity Manager, and Intune
  • Windows services: Activities, Devices, Notifications, Universal Print
  • Dynamics 365 Business Central services
  • Microsoft Partner Center services

This unified API approach means that once you understand the authentication and request patterns for one service (like SharePoint in this article), you can apply similar principles to interact with other Microsoft services as well.

Microsoft Graph API Ecosystem

What You’ll Learn
#

In this comprehensive guide, you’ll learn:

  • How to register an application in Microsoft Entra ID (formerly Azure Active Directory)
  • How to authenticate to Microsoft Graph API
  • How to navigate the SharePoint site structure
  • How to implement file operations (upload, download, update, delete)
  • Best practices for efficient API usage with caching

Who This Guide Is For: Both Beginners getting started with Microsoft Graph API and experienced developers looking for implementation patterns and best practices.

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, ensure you have:

  • 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 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. for backend services, scheduled tasks, or automation scenarios where no user interaction is required.

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 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 additional details, consult the Microsoft Graph API Documentation{:target="_blank" rel=“noopener noreferrer”}.

Get Involved!
#

  • Try the Code: For a complete reference implementation, visit the GitHub Repository{:target="_blank" rel=“noopener noreferrer”}.

  • Follow Us: Stay updated on new developments by following the project on GitHub{:target="_blank" rel=“noopener noreferrer”}.

  • Join the Conversation: What challenges have you faced with Microsoft Graph API? Share your experiences in the comments below!

Related

Building an AI-Driven Chat Application with .NET, Azure OpenAI, and Angular

·11 mins
Introduction # Artificial Intelligence is transforming how we build applications, particularly in creating natural, conversational user experiences. This article guides you through building a full-stack AI chat application using .NET on the backend, Angular for the frontend, and Azure OpenAI for powerful language model capabilities, all connected through real-time SignalR communication.

Using Dapper for Data Access and Repository Pattern

·9 mins
Introduction # In this article, we will explore how the Contact Management Application integrates Dapper for efficient data access. Dapper is a micro-ORM (Object-Relational Mapper) that excels in performance by directly interacting with SQL. We will focus on how Dapper is utilized within the Repository Pattern to ensure clean, maintainable, and efficient access to the database. Additionally, we will explore how Dapper works in tandem with the Unit of Work pattern to manage transactions.

Dependency Injection Setup Across Layers

·6 mins
Introduction # Dependency Injection (DI) is a key design pattern in Clean Architecture that facilitates loose coupling between components. By injecting dependencies rather than hard-coding them, we create more maintainable, testable code where components can be easily swapped or mocked. This article explores how to implement DI across different architectural layers in the Contact Management Application.