🔐 C# Console App: Connect to SharePoint Online and Fetch List Item by Title Using MSAL + REST (No SDKs)
This guide walks you through building a .NET console app that:
✅ Authenticates with Azure AD (Entra ID) using MSAL (interactive sign-in)
✅ Connects to SharePoint Online using REST API
✅ Fetches a list item from a given list by its Title field
✅ Prints details like Id, Created, Modified, Author, and Editor to the console
Everything is handled without using PnP or CSOM, giving you maximum control and portability.
🔧 Use Cases
This lightweight CLI can be used for:
- SharePoint list item lookup scripts
- CLI-based list audits
- Automating cross-site content validation
- Lightweight integrations and testing tools
✅ Prerequisites
| Requirement | Description |
|---|---|
| .NET SDK | Version 6.0 or higher (8.0 tested) |
| Azure AD App Registration | With public client (interactive) enabled |
| Access to SharePoint site | User account must be authorized to read the list |
| List with a “Title” column | The item lookup uses exact match on the Title field |
🏗️ App Registration (Azure Portal)
- Go to Azure Active Directory → App registrations → New registration
- Choose Public client/native platform
- Set Redirect URI:
http://localhost - Under Authentication:
- Enable Public client flows
- Allow
http://localhostas a redirect URI
- Record your:
- Tenant ID
- Client ID
📦 Create the project
dotnet new console -n SpListByTitle
cd SpListByTitle
dotnet add package Microsoft.Identity.Client
🧠 App Logic Overview
- Auth with MSAL interactive
- Derive the SharePoint scope from your site URL
- Call SharePoint REST
_api/web/lists/getbytitle(...)/items?... - Print the result to the console
💡 Program.cs — Full Code
Paste the following into your Program.cs file.
🔒 Update these 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";
✅ The Code
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
namespace SpListByTitle
{
internal class Program
{
// ========================= USER SETTINGS =========================
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";
// =================================================================
static async Task<int> Main(string[] args)
{
Console.WriteLine("=== SharePoint: Connect + Get Item by Title ===");
try
{
string titleToFind = args.Length > 0 ? args[0] : Ask("Title to search: ");
var scopes = BuildScopesFromSite(siteUrl);
var accessToken = await AcquireAccessTokenInteractiveAsync(tenantId, clientId, scopes);
if (string.IsNullOrWhiteSpace(accessToken))
{
Console.WriteLine("Failed to acquire token.");
return 1;
}
var siteName = await GetSiteTitleAsync(siteUrl, accessToken);
Console.WriteLine($"Connected to site: {siteName ?? "(unknown)"}");
var item = await GetFirstItemByTitleAsync(siteUrl, listTitle, titleToFind, accessToken);
if (item is null)
{
Console.WriteLine("No item found with that Title.");
return 0;
}
PrintItemToConsole(item.Value);
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<string?> GetSiteTitleAsync(string siteUrl, string accessToken)
{
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var endpoint = $"{TrimEndSlash(siteUrl)}/_api/web?$select=Title";
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("Title", out var t))
return t.GetString();
}
}
return null;
}
private static async Task<JsonElement?> GetFirstItemByTitleAsync(string siteUrl, string listDisplayName, 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(listDisplayName)}')" +
$"/items?$filter=Title eq '{escapedTitle}'" +
$"&$select=Id,Title,Created,Modified,Author/Title,Editor/Title" +
$"&$expand=Author,Editor&$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(); // VERY IMPORTANT: avoid ObjectDisposedException
}
}
}
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;
private static void PrintItemToConsole(JsonElement item)
{
string GetStr(string name) =>
item.TryGetProperty(name, out var v) && v.ValueKind != JsonValueKind.Null ? v.ToString() : string.Empty;
string id = GetStr("Id");
string title = GetStr("Title");
string created = GetStr("Created");
string modified = GetStr("Modified");
string author = item.TryGetProperty("Author", out var a) && a.TryGetProperty("Title", out var at) ? at.ToString() : "";
string editor = item.TryGetProperty("Editor", out var e) && e.TryGetProperty("Title", out var et) ? et.ToString() : "";
Console.WriteLine("-------------------------------------------------");
Console.WriteLine($"Id : {id}");
Console.WriteLine($"Title : {title}");
Console.WriteLine($"Created : {created}");
Console.WriteLine($"Modified : {modified}");
Console.WriteLine($"Author : {author}");
Console.WriteLine($"Editor : {editor}");
Console.WriteLine("-------------------------------------------------");
}
private static string Ask(string prompt)
{
Console.Write(prompt);
return Console.ReadLine() ?? string.Empty;
}
}
}
🧪 Example Usage
dotnet run -- "Project X"
Or run without args and it will prompt:
Title to search:
✅ Output Example
=== SharePoint: Connect + Get Item by Title ===
Connected to site: Engineering Site
-------------------------------------------------
Id : 42
Title : Project X
Created : 2025-08-12T17:21:36Z
Modified : 2025-09-01T10:15:03Z
Author : Jane Doe
Editor : John Smith
-------------------------------------------------
🧯 Troubleshooting
| Problem | Fix |
|---|---|
| Proxy 502 error | Add UseProxy = false in HttpClientHandler or unset HTTPS_PROXY env var |
| ObjectDisposedException | Make sure to .Clone() any JsonElement you return from inside a using block |
| Admin consent required | Your tenant may require admin to pre-approve delegated scopes |
| 401 Unauthorized | Double-check if your user has access to the site and list |
| Title not found | Make sure the Title is exactly correct (case-sensitive, no extra spaces) |
