🔐 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

RequirementDescription
.NET SDKVersion 6.0 or higher (8.0 tested)
Azure AD App RegistrationWith public client (interactive) enabled
Access to SharePoint siteUser account must be authorized to read the list
List with a “Title” columnThe item lookup uses exact match on the Title field

🏗️ App Registration (Azure Portal)

  1. Go to Azure Active Directory → App registrations → New registration
  2. Choose Public client/native platform
  3. Set Redirect URI: http://localhost
  4. Under Authentication:
    • Enable Public client flows
    • Allow http://localhost as a redirect URI
  5. 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

ProblemFix
Proxy 502 errorAdd UseProxy = false in HttpClientHandler or unset HTTPS_PROXY env var
ObjectDisposedExceptionMake sure to .Clone() any JsonElement you return from inside a using block
Admin consent requiredYour tenant may require admin to pre-approve delegated scopes
401 UnauthorizedDouble-check if your user has access to the site and list
Title not foundMake sure the Title is exactly correct (case-sensitive, no extra spaces)

Edvaldo Guimrães Filho Avatar

Published by