📎 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
| Step | Function |
|---|---|
| 🔐 Authenticates | MSAL interactive login |
| 🔍 Finds item | Searches SharePoint list by Title |
| 📎 Uploads | Attaches a file to the item |
| ⚙️ Mode | All hardcoded for PoC (can be parameterized later) |
🧰 Requirements
| Requirement | Notes |
|---|---|
| .NET SDK 6.0+ | Test with dotnet --version |
| App Registration in Entra ID | Must allow public client flows |
SharePoint List with Title field | Will be queried via REST |
A .txt file to attach | Called attachmenttest.txt in this example |
🔐 Step 1: Create App Registration in Azure (Entra ID)
- Go to Azure Portal > Entra ID > App registrations > New registration
- Choose any name, e.g.,
SpAttachToListItem - Supported account type: Single Tenant or as needed
- Redirect URI: Mobile & desktop app →
http://localhost - After creating:
- Go to Authentication
- Enable Public client flows
- Ensure
http://localhostis listed as redirect URI
- 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
| API | Purpose |
|---|---|
/_api/web/lists/getbytitle('List')/items?$filter=Title eq '...' | Search for item |
/_api/web/lists/getbytitle('List')/items(ID)/AttachmentFiles/add(...) | Upload attachment |
🧯 Troubleshooting
| Error | Solution |
|---|---|
401 Unauthorized | Check user permissions on site and list |
ObjectDisposedException | Use .Clone() on JsonElement before returning |
502 Proxy error | Disable system proxy or Fiddler |
| File not uploaded | Ensure 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
