📎 C# Console App: Attach a File to a SharePoint List Item via REST + MSAL (No SDK)

This guide walks you through building a .NET Console App that connects to SharePoint Online using MSAL (interactive authentication) and uploads a file as an attachment to a list item — all via raw REST API, without using CSOM or PnP SDKs.

This is perfect for:

  • Lightweight automation
  • PoC (Proof of Concept)
  • CLI-based SharePoint tools
  • Scripting file uploads to items by title

✅ What This App Does

StepFunction
🔐 AuthenticatesMSAL interactive login
🔍 Finds itemSearches SharePoint list by Title
📎 UploadsAttaches a file to the item
⚙️ ModeAll hardcoded for PoC (can be parameterized later)

🧰 Requirements

RequirementNotes
.NET SDK 6.0+Test with dotnet --version
App Registration in Entra IDMust allow public client flows
SharePoint List with Title fieldWill be queried via REST
A .txt file to attachCalled attachmenttest.txt in this example

🔐 Step 1: Create App Registration in Azure (Entra ID)

  1. Go to Azure Portal > Entra ID > App registrations > New registration
  2. Choose any name, e.g., SpAttachToListItem
  3. Supported account type: Single Tenant or as needed
  4. Redirect URI: Mobile & desktop app → http://localhost
  5. After creating:
    • Go to Authentication
    • Enable Public client flows
    • Ensure http://localhost is listed as redirect URI
  6. Save your:
    • Tenant ID
    • Client ID

🏗 Step 2: Create the Console App and File

dotnet new console -n SpAttachToListItem
cd SpAttachToListItem
dotnet add package Microsoft.Identity.Client

Create the file to attach

In the same folder:

📄 attachmenttest.txt:

This is a test attachment uploaded via SharePoint REST API and MSAL.


📄 Step 3: Program.cs (Full Code)

Replace the placeholders with your real values:

private static readonly string tenantId = "YOUR_TENANT_ID";
private static readonly string clientId = "YOUR_CLIENT_ID";
private static readonly string siteUrl = "https://yourtenant.sharepoint.com/sites/YourSite";
private static readonly string listTitle = "Your List Name";
private static readonly string titleToFind = "My Target Item";

Here is the full Program.cs:

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Identity.Client;

namespace SpAttachToListItem
{
    internal class Program
    {
        private static readonly string tenantId = "YOUR_TENANT_ID";
        private static readonly string clientId = "YOUR_CLIENT_ID";
        private static readonly string siteUrl = "https://yourtenant.sharepoint.com/sites/YourSite";
        private static readonly string listTitle = "Your List Name";
        private static readonly string filePath = "attachmenttest.txt";
        private static readonly string titleToFind = "My Target Item";

        static async Task<int> Main()
        {
            Console.WriteLine("=== SharePoint: Attach File to List Item ===");
            Console.WriteLine($"Looking for item with Title: \"{titleToFind}\"");

            try
            {
                var scopes = BuildScopesFromSite(siteUrl);

                var token = await AcquireAccessTokenInteractiveAsync(tenantId, clientId, scopes);
                if (string.IsNullOrWhiteSpace(token))
                {
                    Console.WriteLine("Failed to acquire token.");
                    return 1;
                }

                var item = await GetFirstItemByTitleAsync(siteUrl, listTitle, titleToFind, token);
                if (item == null)
                {
                    Console.WriteLine("No item found with that Title.");
                    return 0;
                }

                int itemId = item.Value.TryGetProperty("Id", out var idProp) ? idProp.GetInt32() : -1;
                if (itemId == -1)
                {
                    Console.WriteLine("Could not read item ID.");
                    return 1;
                }

                Console.WriteLine($"Found item with ID: {itemId}");

                await UploadAttachmentAsync(siteUrl, listTitle, itemId, filePath, token);

                Console.WriteLine("✅ Attachment uploaded successfully.");
                return 0;
            }
            catch (Exception ex)
            {
                Console.WriteLine("❌ Error: " + ex.Message);
                Console.WriteLine(ex);
                return 2;
            }
        }

        private static async Task<string> AcquireAccessTokenInteractiveAsync(string tenantId, string clientId, string[] scopes)
        {
            var app = PublicClientApplicationBuilder.Create(clientId)
                .WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
                .WithRedirectUri("http://localhost")
                .Build();

            try
            {
                var result = await app.AcquireTokenInteractive(scopes)
                    .WithPrompt(Prompt.SelectAccount)
                    .ExecuteAsync();
                return result.AccessToken;
            }
            catch (MsalException mex)
            {
                Console.WriteLine("MSAL error: " + mex.Message);
                return string.Empty;
            }
        }

        private static async Task<JsonElement?> GetFirstItemByTitleAsync(string siteUrl, string listName, string titleValue, string accessToken)
        {
            using (var http = new HttpClient())
            {
                http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                string escapedTitle = titleValue.Replace("'", "''");
                string endpoint =
                    $"{TrimEndSlash(siteUrl)}/_api/web/lists/getbytitle('{Uri.EscapeDataString(listName)}')" +
                    $"/items?$filter=Title eq '{escapedTitle}'&$select=Id,Title&$top=1";

                var res = await http.GetAsync(endpoint);
                var body = await res.Content.ReadAsStringAsync();

                if (!res.IsSuccessStatusCode)
                    throw new Exception($"SharePoint error {(int)res.StatusCode}: {res.ReasonPhrase}\n{body}");

                using (var doc = JsonDocument.Parse(body))
                {
                    if (!doc.RootElement.TryGetProperty("value", out var arr) || arr.GetArrayLength() == 0)
                        return null;

                    return arr[0].Clone(); // Prevents ObjectDisposedException
                }
            }
        }

        private static async Task UploadAttachmentAsync(string siteUrl, string listName, int itemId, string filePath, string accessToken)
        {
            if (!File.Exists(filePath))
                throw new FileNotFoundException($"File not found: {filePath}");

            byte[] fileBytes = await File.ReadAllBytesAsync(filePath);
            string fileName = Path.GetFileName(filePath);

            string endpoint =
                $"{TrimEndSlash(siteUrl)}/_api/web/lists/getbytitle('{Uri.EscapeDataString(listName)}')" +
                $"/items({itemId})/AttachmentFiles/add(FileName='{Uri.EscapeDataString(fileName)}')";

            using (var http = new HttpClient())
            {
                http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

                using (var content = new ByteArrayContent(fileBytes))
                {
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

                    var res = await http.PostAsync(endpoint, content);
                    var body = await res.Content.ReadAsStringAsync();

                    if (!res.IsSuccessStatusCode)
                        throw new Exception($"Upload failed: {(int)res.StatusCode} - {res.ReasonPhrase}\n{body}");
                }
            }
        }

        private static string[] BuildScopesFromSite(string siteUrl)
        {
            var resource = new Uri(siteUrl).GetLeftPart(UriPartial.Authority);
            return new[] { $"{resource}/AllSites.FullControl" };
        }

        private static string TrimEndSlash(string url) =>
            url.EndsWith("/") ? url.TrimEnd('/') : url;
    }
}


▶️ Run It

dotnet run

Expected output:

=== SharePoint: Attach File to List Item ===
Looking for item with Title: "My Target Item"
Found item with ID: 47
✅ Attachment uploaded successfully.


🧪 REST APIs used

APIPurpose
/_api/web/lists/getbytitle('List')/items?$filter=Title eq '...'Search for item
/_api/web/lists/getbytitle('List')/items(ID)/AttachmentFiles/add(...)Upload attachment

🧯 Troubleshooting

ErrorSolution
401 UnauthorizedCheck user permissions on site and list
ObjectDisposedExceptionUse .Clone() on JsonElement before returning
502 Proxy errorDisable system proxy or Fiddler
File not uploadedEnsure file exists and user has Contribute permissions

🔄 What’s Next?

You can evolve this project into a full CLI or tool:

  • Accept file path and Title via command-line args
  • Upload multiple files
  • Log results to file or telemetry
  • Auto-create items if not found

Edvaldo Guimrães Filho Avatar

Published by