Live training: next batches coming soon. Join the waitlist:Waitlists open:Developer Training →Developer →Copilot Studio Training →Copilot Studio →

SharePoint CSOM Tutorial: C# Client Object Model Examples for SharePoint Online

Many SharePoint CSOM guides still connect with SharePointOnlineCredentials and a password. SharePoint Online no longer accepts that sign-in, so the code fails before it reads a single list.

The better way: add the Microsoft.SharePointOnline.CSOM package to a .NET app, get an Entra ID access token, and attach it to every ClientContext request.

In this tutorial, we’ll cover what SharePoint CSOM is, how it differs from the server object model, and how to connect with MSAL or PnP.Framework.

Then we’ll create a list, add, query, update and delete items in C#, and fix the errors you’ll hit, including InvalidClientQueryException.

What is CSOM in SharePoint?

CSOM (the client-side object model) is a set of .NET classes that let code outside SharePoint work with sites, lists, items and files. Your app queues requests, and ExecuteQuery() sends them in one batch. The same model exists for JavaScript (the JSOM object model) and as the SharePoint REST API.

The “server-side object model” is the older Microsoft.SharePoint API (SPSite, SPWeb) that only runs on a SharePoint server inside the farm.

Server object modelClient object model (CSOM)
Where code runsOn a SharePoint Server in the farmAnywhere: a PC, a server, Azure Functions
How it’s deployedFarm solution (.wsp)A normal .NET app with a NuGet package
SharePoint OnlineNot availableYes (the .NET Standard version)

For SPSite and SPWeb on-premises, see my SharePoint server object model tutorial.

The sample list we’ll use

Every example works on one list on https://spguides.sharepoint.com/sites/SPGuides: SPGDemo CSOM Tasks, which the C# code below creates.

TitleStatus (Choice)DueDateCost (Currency)
Renew SSL certificateNot Started11/02/2026$249.00
Order laptops for new hiresIn Progress10/30/2026$4,380.00
Update Teams phone policyIn Progress11/13/2026$0.00
Archive 2025 project sitesNot Started12/04/2026$0.00

Method 1: Connect with an MSAL access token (Recommended)

Create a console app and add the packages (any current .NET SDK works):

dotnet new console -n SPGuidesCsom -o SPGuidesCsom
cd SPGuidesCsom
dotnet add package Microsoft.SharePointOnline.CSOM
dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.SharePointOnline.CSOM adding the SharePoint CSOM NuGet package to a .NET console app

Register an Entra ID app as a public client with the redirect URI http://localhost and a delegated SharePoint permission such as AllSites.Manage. Redirect errors? See the AADSTS500113 reply address fix.

using Microsoft.Identity.Client;
using Microsoft.SharePoint.Client;

string siteUrl  = "https://spguides.sharepoint.com/sites/SPGuides";
string clientId = "<your-app-client-id>";
string tenantId = "<your-tenant-id>";

// Delegated permission on the SharePoint API, for example AllSites.Manage
string[] scopes = { "https://spguides.sharepoint.com/AllSites.Manage" };

IPublicClientApplication app = PublicClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantId)
    .WithRedirectUri("http://localhost")
    .Build();

AuthenticationResult result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();

using var ctx = new ClientContext(siteUrl);
ctx.ExecutingWebRequest += (sender, e) =>
    e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + result.AccessToken;

How does this work?

MSAL returns a SharePoint token, and ExecutingWebRequest adds it as a Bearer header to every call.

Now read the site, the user and the lists in one batch:

Web web = ctx.Web;
User me = web.CurrentUser;
ctx.Load(web, w => w.Title, w => w.ServerRelativeUrl, w => w.Language);
ctx.Load(me, u => u.Title);
var lists = ctx.LoadQuery(web.Lists
    .Where(l => l.Hidden == false)
    .Include(l => l.Title, l => l.ItemCount));
ctx.ExecuteQuery();   // one round trip for all three requests

Console.WriteLine($"Site title : {web.Title}");
Console.WriteLine($"Site URL   : {web.ServerRelativeUrl}");
Console.WriteLine($"Signed in  : {me.Title}");
foreach (List l in lists.Where(l => l.Title.StartsWith("SPGDemo")))
    Console.WriteLine($"{l.Title,-25} {l.ItemCount,5} items");

For my test runs, I passed in a PnP PowerShell token instead. All three requests went out in one round trip:

# PowerShell: borrow a token from your PnP PowerShell sign-in for quick tests
$env:SPO_SITE  = "https://spguides.sharepoint.com/sites/SPGuides"
$env:SPO_TOKEN = Get-PnPAccessToken -ResourceTypeName SharePoint
dotnet run -- web
C# CSOM console app reading the SharePoint site title, signed-in user and lists in one request

Method 2: Let PnP.Framework handle sign-in

The open-source PnP.Framework library wraps MSAL and hands you a ready ClientContext:

// dotnet add package PnP.Framework
var authManager = PnP.Framework.AuthenticationManager.CreateWithInteractiveLogin(
    "<your-app-client-id>", tenantId: "<your-tenant-id>");

using ClientContext ctx = await authManager.GetContextAsync(
    "https://spguides.sharepoint.com/sites/SPGuides");

ctx.Load(ctx.Web, w => w.Title);
await ctx.ExecuteQueryAsync();
Console.WriteLine(ctx.Web.Title);

How does this work?

CreateWithInteractiveLogin signs you in through the browser. For unattended jobs, use CreateWithCertificate: Entra ID app-only access to SharePoint needs a certificate. PnP PowerShell uses the same library.

Method 3: SharePointOnlineCredentials (legacy, don’t use)

// LEGACY - fails in SharePoint Online since IDCRL was retired on 05/01/2026
var password = new SecureString();
foreach (char c in "your-password") password.AppendChar(c);

var ctx = new ClientContext("https://spguides.sharepoint.com/sites/SPGuides");
ctx.Credentials = new SharePointOnlineCredentials("admin@spguides.onmicrosoft.com", password);

It signs in with IDCRL, which SharePoint Online retired on 05/01/2026; Azure ACS secrets stopped on 04/02/2026. Use it only on-premises.

Create a list and add items with CSOM

This creates the sample list with three columns:

var info = new ListCreationInformation
{
    Title = "SPGDemo CSOM Tasks",
    TemplateType = (int)ListTemplateType.GenericList
};
List list = ctx.Web.Lists.Add(info);
list.Description = "Tasks created with CSOM in C#";
list.Update();

list.Fields.AddFieldAsXml("<Field Type='Choice' DisplayName='Status' Name='Status'>" +
    "<CHOICES><CHOICE>Not Started</CHOICE><CHOICE>In Progress</CHOICE><CHOICE>Completed</CHOICE></CHOICES>" +
    "<Default>Not Started</Default></Field>", true, AddFieldOptions.AddFieldInternalNameHint);
list.Fields.AddFieldAsXml("<Field Type='DateTime' DisplayName='DueDate' Name='DueDate' Format='DateOnly' />",
    true, AddFieldOptions.AddFieldInternalNameHint);
list.Fields.AddFieldAsXml("<Field Type='Currency' DisplayName='Cost' Name='Cost' LCID='1033' />",
    true, AddFieldOptions.AddFieldInternalNameHint);
ctx.ExecuteQuery();

And this adds the four rows:

List tasks = ctx.Web.Lists.GetByTitle("SPGDemo CSOM Tasks");
var rows = new (string Title, string Status, DateTime Due, double Cost)[]
{
    ("Renew SSL certificate",       "Not Started", new DateTime(2026, 11, 2),  249.00),
    ("Order laptops for new hires", "In Progress", new DateTime(2026, 10, 30), 4380.00),
    ("Update Teams phone policy",   "In Progress", new DateTime(2026, 11, 13), 0),
    ("Archive 2025 project sites",  "Not Started", new DateTime(2026, 12, 4),  0),
};

foreach (var r in rows)
{
    ListItem item = tasks.AddItem(new ListItemCreationInformation());
    item["Title"] = r.Title;
    item["Status"] = r.Status;
    item["DueDate"] = r.Due;
    item["Cost"] = r.Cost;
    item.Update();
    ctx.Load(item, i => i.Id);
}
ctx.ExecuteQuery();   // all four items in one batch
CSOM C# creating the SPGDemo CSOM Tasks list and adding four list items in one batch

How does this work?

Lists.Add() and AddItem() only queue work; nothing reaches SharePoint until ExecuteQuery(). That’s why all four items took just 1 request.

Read items with a CAML query

To get In Progress tasks, soonest due first, pass CAML to GetItems():

var query = new CamlQuery
{
    ViewXml = @"<View>
  <Query>
    <Where>
      <Eq><FieldRef Name='Status' /><Value Type='Choice'>In Progress</Value></Eq>
    </Where>
    <OrderBy><FieldRef Name='DueDate' Ascending='TRUE' /></OrderBy>
  </Query>
  <ViewFields><FieldRef Name='Title' /><FieldRef Name='DueDate' /><FieldRef Name='Cost' /></ViewFields>
  <RowLimit>100</RowLimit>
</View>"
};
ListItemCollection items = tasks.GetItems(query);
ctx.Load(items);
ctx.ExecuteQuery();

foreach (ListItem i in items)
{
    var due = ((DateTime)i["DueDate"]).ToLocalTime();
    Console.WriteLine($"#{i.Id} {i["Title"],-30} due {due:MM/dd/yyyy} {(double)i["Cost"],10:C}");
}
CAML query in C# CSOM returning In Progress items sorted by due date with USD costs

How does this work?

Where matched the two In Progress rows and OrderBy put 10/30/2026 first. DueDate comes back in UTC, so I call ToLocalTime().

Pro Tip: Test CAML before pasting it into C#; my CAML query builder guide shows how. Past 5,000 items, filter on indexed columns and page with ListItemCollectionPosition.

Update and delete list items

// Update: change one column and save
ListItem item = tasks.GetItemById(2);
item["Status"] = "Completed";
item.Update();
ctx.ExecuteQuery();

// Delete: Recycle() sends the item to the recycle bin; DeleteObject() removes it for good
ListItem old = tasks.GetItemById(4);
var recycleId = old.Recycle();
ctx.ExecuteQuery();
Console.WriteLine($"Recycle bin ID: {recycleId.Value}");
CSOM C# updating a SharePoint list item status and moving another item to the recycle bin

How does this work?

Item 2 is now Completed and item 4 went to the SharePoint recycle bin. I prefer Recycle() because users can restore the item.

SPGDemo CSOM Tasks SharePoint list after the CSOM update and recycle, showing Status, DueDate and Cost columns

Common CSOM errors and how to fix them

CSOM PropertyOrFieldNotInitializedException and ServerException list does not exist errors in a C# console app
  • PropertyOrFieldNotInitializedException: you read a property you didn’t load. Name it in Load(): ctx.Load(web, w => w.Title, w => w.Description).
  • ServerException: List ‘…’ does not exist: the list title is wrong. Check it, or use GetById().
  • The ‘StartsWith’ member cannot be used in the expression: CSOM filters allow only simple comparisons. Filter the rest with LINQ after ExecuteQuery().
  • 401 Unauthorized: the token scope must use your https://<tenant>.sharepoint.com host, not Microsoft Graph.
  • Server relative URLs must start with SPWeb.ServerRelativeUrl: see the server relative URL error fix.

Need list events? The .NET Standard CSOM has no event receivers; use SharePoint webhooks instead of event receivers.

Fix Microsoft.SharePoint.Client.InvalidClientQueryException

You’ll usually see this from SharePoint REST API CRUD calls: SharePoint couldn’t parse the URL or body. Here are three causes I reproduced:

Microsoft.SharePoint.Client.InvalidClientQueryException from three SharePoint REST requests: undefined item ID, wrong property, bad filter
  • “…/items(undefined)” is not valid: the item ID was never set, often items.Id instead of items[i].Id in a loop.
  • The property ‘Titel’ does not exist: a column name in the body is misspelled or not the internal name.
  • “Status eq In Progress” is not valid: quote text in $filter: Status eq 'In Progress'.
  • An entry without a type name was found: an odata=verbose body without __metadata; see the entry without a type name fix.
// Wrong: items is the whole array, so items.Id is undefined -> .../items(undefined)
var itemId = items.Id;

// Right: read the ID of the current row
var itemId = items[i].Id;

A missing X-RequestDigest is a different error: 403 with the security validation for this page is invalid. Bearer-token calls don’t need a digest.

Which method should you use?

SituationUse
New C# tool, SharePoint OnlineCSOM + MSAL token (Method 1)
Sign-in handled for youPnP.Framework (Method 2)
Unattended jobEntra ID app with a certificate
SharePoint Server on-premises.NET Framework CSOM
Code inside SharePoint pagesSPFx with PnPjs or REST

For browser code, start with the SPFx tutorials.

Things to Keep in Mind

  • Batch your work: one ExecuteQuery() per group of changes means fewer round trips and less throttling (HTTP 429).
  • Load only what you need: Load(web) returns a default property set, not everything.

Want to keep going with SharePoint development? These guides pick up where this one ends:

Frequently Asked Questions

What is CSOM in SharePoint?

CSOM, the client-side object model, is a set of .NET classes that lets apps outside SharePoint work with sites, lists, items and files, sending batched requests on ExecuteQuery.

What is the difference between the server object model and the client object model?

The server object model (SPSite, SPWeb) runs only on a SharePoint Server in the farm. CSOM runs anywhere over HTTPS and works with SharePoint Online.

Does SharePointOnlineCredentials still work in SharePoint Online?

No. It relies on the legacy IDCRL protocol, which Microsoft retired in SharePoint Online on 05/01/2026. Use an Entra ID access token, as shown in Using CSOM for .NET Standard.

Can I use CSOM with a client ID and client secret?

Not for app-only access. Entra ID app-only access to SharePoint needs a certificate, and Azure ACS secrets stopped working on 04/02/2026 (Azure ACS retirement).

What causes Microsoft.SharePoint.Client.InvalidClientQueryException?

SharePoint couldn’t parse your REST request: an undefined ID, a misspelled column, unquoted $filter text, or a verbose body without __metadata.

3 thoughts on “SharePoint CSOM Tutorial: C# Client Object Model Examples for SharePoint Online”

  1. A Great step-by-step tutorial. Thanks!

    But I am getting a problem: Whenever I try to connect, I receive an error 400 from the server:

    System.Net.WebException: ‘The remote server returned an error: (400) Bad Request.’

    This exception was originally thrown at this call stack:
    System.Net.HttpWebRequest.GetResponse()
    Microsoft.SharePoint.Client.SPWebRequestExecutor.Execute()
    Microsoft.SharePoint.Client.ClientContext.GetFormDigestInfoPrivate()
    Microsoft.SharePoint.Client.ClientContext.EnsureFormDigest()
    Microsoft.SharePoint.Client.ClientContext.ExecuteQuery()
    CSOM_SharePoint.Program.Main(string[]) in Program.cs

    Any idea on how to solve this?

    Thank you in advance

    • I just found out where I got it wrong: I created the project using .NET Core instead of .NET Framework. Once I created a new solution using .NET Framework, it worked correclty.

Leave a Comment