Connect & Query SharePoint Online from C# (.NET) with Interactive Login — Get List Item by Title

This post shows how to build a fresh, minimal .NET console app that:

  1. Authenticates interactively with Entra ID (Azure AD) using MSAL, and
  2. Fetches a list item by Title from SharePoint Online using the classic REST API.

No SDK lock-in, no tenant names hardcoded. The scope is derived from your siteUrl so this template is safe to reuse across clients/tenants.


What you’ll build

  • Project name: SpListByTitle
  • MSAL interactive sign-in (public client)
  • Dynamic SharePoint scope built from siteUrlhttps://<tenant>.sharepoint.com/AllSites.FullControl
  • REST query:
    /_api/web/lists/getbytitle('<List>')/items?$filter=Title eq '<value>'&$top=1
  • Console output: Id, Title, Created, Modified, Author, Editor

Prerequisites

  • .NET SDK 6.0+ (8.0 recommended): dotnet --version
  • An App Registration in Entra ID configured as a Public client (native)
    • Add platform Mobile and desktop applications
    • Redirect URI: http://localhost
    • Enable public client flows
  • A user who can access the target SharePoint site

This template uses delegated permissions via interactive sign-in. Consent may be required depending on your tenant policy.


1) Create the project

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


2) Full source code (single file)

Replace Program.cs with the code below.
Fill in: tenantId, clientId, siteUrl, listTitle.
Run via: dotnet run -- "Exact Title Here"

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 =========================
        // Entra ID (Azure AD)
        private static readonly string tenantId = "YOUR_TENANT_ID_GUID";
        private static readonly string clientId = "YOUR_PUBLIC_CLIENT_APP_ID_GUID";

        // SharePoint target (authority comes from this URL)
        private static readonly string siteUrl   = "https://contoso.sharepoint.com/sites/YourSite";
        private static readonly string listTitle = "Your List Display Name";
        // =================================================================

        static async Task<int> Main(string[] args)
        {
            Console.WriteLine("=== SharePoint: Connect + Get Item by Title ===");

            try
            {
                // Ask for Title (CLI arg or prompt)
                string titleToFind = args.Length > 0 ? args[0] : Ask("Title to search: ");

                // Build tenant-agnostic scope from siteUrl (e.g., https://contoso.sharepoint.com/AllSites.FullControl)
                var scopes = BuildScopesFromSite(siteUrl);

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

                // (Optional) quick connectivity probe (prints site title)
                var siteName = await GetSiteTitleAsync(siteUrl, accessToken);
                Console.WriteLine($"Connected to site: {siteName ?? "(unknown)"}");

                // 2) Fetch the first item where Title == titleToFind
                var item = await GetFirstItemByTitleAsync(siteUrl, listTitle, titleToFind, accessToken);

                if (item is null)
                {
                    Console.WriteLine("No item found with that Title.");
                    return 0;
                }

                // 3) Pretty-print selected fields
                PrintItemToConsole(item.Value);

                return 0;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error: " + ex.Message);
                Console.WriteLine(ex);
                return 2;
            }
        }

        // ---------------------------- Auth ----------------------------
        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;
            }
        }

        // ---------------------- Connectivity Probe ---------------------
        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.Clear();
                http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                var endpoint = $"{TrimEndSlash(siteUrl)}/_api/web?$select=Title";
                using (var req = new HttpRequestMessage(HttpMethod.Get, endpoint))
                {
                    req.Headers.Add("Accept", "application/json;odata=nometadata");

                    using (var res = await http.SendAsync(req))
                    {
                        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;
        }

        // ---------------------- List Item by Title ---------------------
        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.Clear();
                http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Escape single quotes for OData string literals
                string escapedTitleLiteral = (titleValue ?? string.Empty).Replace("'", "''");

                string endpoint =
                    $"{TrimEndSlash(siteUrl)}/_api/web/lists/getbytitle('{Uri.EscapeDataString(listDisplayName)}')" +
                    $"/items?$filter=Title eq '{escapedTitleLiteral}'" +
                    $"&$select=Id,Title,Created,Modified,Author/Title,Editor/Title" +
                    $"&$expand=Author,Editor&$top=1";

                using (var req = new HttpRequestMessage(HttpMethod.Get, endpoint))
                {
                    req.Headers.Add("Accept", "application/json;odata=nometadata");

                    using (var res = await http.SendAsync(req))
                    {
                        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))
                        {
                            // odata=nometadata shape: { "value": [ ... ] }
                            if (!doc.RootElement.TryGetProperty("value", out var arr) || arr.GetArrayLength() == 0)
                                return null;

                            return arr[0];
                        }
                    }
                }
            }
        }

        // --------------------------- Helpers --------------------------
        private static string[] BuildScopesFromSite(string siteUrl)
        {
            var resource = new Uri(siteUrl).GetLeftPart(UriPartial.Authority); // e.g., https://contoso.sharepoint.com
            return new[] { $"{resource}/AllSites.FullControl" };
        }

        private static string TrimEndSlash(string url)
        {
            if (string.IsNullOrWhiteSpace(url)) return url;
            return url.EndsWith("/") ? url.TrimEnd('/') : url;
        }

        private static void PrintItemToConsole(JsonElement item)
        {
            string GetStr(string name)
            {
                return 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.ValueKind == JsonValueKind.Object
                               && a.TryGetProperty("Title", out var aTitle)
                               ? aTitle.ToString()
                               : string.Empty;

            string editor    = item.TryGetProperty("Editor", out var e) && e.ValueKind == JsonValueKind.Object
                               && e.TryGetProperty("Title", out var eTitle)
                               ? eTitle.ToString()
                               : string.Empty;

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


3) Run it

dotnet run -- "Exact Title Here"

If you omit the argument, it will prompt:

Title to search:

Expected output (example):

=== SharePoint: Connect + Get Item by Title ===
Connected to site: Projects Portal
-------------------------------------------------
Id       : 42
Title    : Exact Title Here
Created  : 2025-08-12T17:21:36Z
Modified : 2025-09-01T10:15:03Z
Author   : Jane Doe
Editor   : John Smith
-------------------------------------------------


Customizations (quick)

  • All matches instead of first: remove &$top=1 and iterate the value[] array.
  • Contains / StartsWith:
    • substringof('needle', Title)
    • startswith(Title,'prefix')
      Mind performance; add indexes where appropriate.
  • More fields: add to $select and $expand (e.g., Editor/Email, custom columns).

Troubleshooting

SymptomLikely CauseFix
MSAL error: admin_consent_requiredTenant requires admin consent for delegated SPO scopesAsk an admin to grant consent or pre-consent the scope
Browser returns error pageRedirect URI mismatchEnsure http://localhost is configured under Mobile and desktop applications
401 Unauthorized from RESTWrong scope, missing header, or user lacks accessKeep dynamic scope, check Authorization: Bearer <token>, confirm user/site permissions
invalid_clientApp not configured as public clientEnable public client flows in the app registration
Title with ' (apostrophes) failsUnescaped OData literalThe code doubles single quotes; keep that method in place

Summary — Steps

#StepCommand / Action
1Create console appdotnet new console -n SpListByTitle
2Add MSALdotnet add package Microsoft.Identity.Client
3ConfigureSet tenantId, clientId, siteUrl, listTitle
4ScopeDerived from siteUrl<authority>/AllSites.FullControl
5Rundotnet run -- "Exact Title Here"
6ResultPrints first matching item’s key fields

Summary — Technical

TopicValue
Auth flowMSAL interactive (Public client, system browser)
AuthorityAzurePublic + your tenantId
Redirect URIhttp://localhost
Scope<siteUrl authority>/AllSites.FullControl (built at runtime)
REST endpoint/_api/web/lists/getbytitle('<List>')/items?$filter=Title eq '...'&$top=1
HeadersAuthorization: Bearer <token>, Accept: application/json;odata=nometadata
JSON parsingSystem.Text.Json (value[0])
Output fieldsId, Title, Created, Modified, Author/Title, Editor/Title

Edvaldo Guimrães Filho Avatar

Published by