Nitin Kumar SinghSolutions Architect

Type to search. to move, Enter to open.

    move open esc close
    Deep Dive.NET

    Implementing SharePoint File CRUD Operations using Microsoft Graph API

    Learn to implement file upload, download, search, and deletion in SharePoint using Microsoft Graph API with .NET, including authentication and error handling.

    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
    Microsoft Graph API Ecosystem

    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:

    CallersSpo.GraphApiExternal servicesThe core library, in orderClient applicationSpo.WebApiHTTP surfaceSpo.GraphApiGraphApiClientFactoryGraphApiClientbuilds the authenticated requestGraphApiAuthenticationHandlerOAuth 2.0 token per requestMicrosoft Graph APISharePoint OnlineusesOAuth 2.0
    The library is the only thing that talks to Graph. Everything above it hands off; everything below it is Microsoft's. The authentication handler sits beside the client rather than in front of it, because it attaches a token to a request the client has already built.

    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 portal1.

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

    References

    1. Microsoft Entra ID in the Azure portal — portal.azure.com

    2. sharepoint-graph-api on GitHub — github.com

    3. Microsoft Graph SharePoint docs — learn.microsoft.com

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.