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 model | Client object model (CSOM) | |
|---|---|---|
| Where code runs | On a SharePoint Server in the farm | Anywhere: a PC, a server, Azure Functions |
| How it’s deployed | Farm solution (.wsp) | A normal .NET app with a NuGet package |
| SharePoint Online | Not available | Yes (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.
| Title | Status (Choice) | DueDate | Cost (Currency) |
|---|---|---|---|
| Renew SSL certificate | Not Started | 11/02/2026 | $249.00 |
| Order laptops for new hires | In Progress | 10/30/2026 | $4,380.00 |
| Update Teams phone policy | In Progress | 11/13/2026 | $0.00 |
| Archive 2025 project sites | Not Started | 12/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

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

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

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

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

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.

Common CSOM errors and how to fix them

- 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.comhost, 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:

- “…/items(undefined)” is not valid: the item ID was never set, often
items.Idinstead ofitems[i].Idin 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=verbosebody 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?
| Situation | Use |
|---|---|
| New C# tool, SharePoint Online | CSOM + MSAL token (Method 1) |
| Sign-in handled for you | PnP.Framework (Method 2) |
| Unattended job | Entra ID app with a certificate |
| SharePoint Server on-premises | .NET Framework CSOM |
| Code inside SharePoint pages | SPFx 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.

Hey! I’m Bijay Kumar, founder of SPGuides.com and a Microsoft Business Applications MVP (Power Automate, Power Apps). I launched this site in 2020 because I truly enjoy working with SharePoint, Power Platform, and SharePoint Framework (SPFx), and wanted to share that passion through step-by-step tutorials, guides, and training videos. My mission is to help you learn these technologies so you can utilize SharePoint, enhance productivity, and potentially build business solutions along the way.
Thanks Bijay. It was very helpful
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.