Skip to content
SharePoint Integration

SharePoint CSOM: How It Works and Where It Still Fits

The load-and-execute model, the threshold, and why CSOM code written before April 2026 may no longer authenticate.

Rajiv KumarJanuary 2, 20195 min read

CSOM batches work locally and dispatches it on ExecuteQuery — which explains its performance characteristics and its most confusing exception. Here is how to use it well, and what the Azure ACS retirement changed.

SharePoint offers several ways to read and write data from code, and choosing between them is mostly a question of where your code runs. The Client-Side Object Model is what you use when it runs somewhere other than the SharePoint server itself — a console application, a service, a desktop tool, a script.

That single fact explains almost everything about how CSOM behaves, including the parts that surprise people the first time.

What CSOM actually is

CSOM is a client library that talks to SharePoint over HTTP. You write code that looks like it is working with SharePoint objects directly, but nothing happens on the server until you say so. Requests are batched locally and sent when you call ExecuteQuery.

This is the design decision everything else follows from. A round trip to SharePoint is expensive, so CSOM makes you accumulate work and dispatch it deliberately rather than chatting continuously.

The load-and-execute pattern

The behaviour that catches everyone: retrieving an object does not populate it.

using var context = new ClientContext("https://contoso.sharepoint.com/sites/finance");
context.Credentials = credentials;

Web web = context.Web;

// Nothing has been fetched. web.Title throws PropertyOrFieldNotInitializedException.
context.Load(web, w => w.Title, w => w.Description);
context.ExecuteQuery();

// Now it is populated.
Console.WriteLine(web.Title);

Two things are worth internalising here.

Request only what you need. context.Load(web) without a lambda retrieves every property SharePoint is willing to send, which is a much larger payload than the two fields you wanted. Naming properties explicitly is the single most effective performance habit in CSOM.

Batch aggressively. Every ExecuteQuery is a network round trip. Loading ten lists in a loop with an execute inside it makes ten round trips; loading all ten then executing once makes one.

// One round trip for all of it
ListCollection lists = context.Web.Lists;
context.Load(lists, ls => ls.Include(l => l.Title, l => l.ItemCount, l => l.BaseTemplate));

List invoices = lists.GetByTitle("Invoices");
context.Load(invoices, l => l.EnableVersioning, l => l.DefaultViewUrl);

context.ExecuteQuery();

Querying items without hitting the threshold

Reading list items uses CAML, and the list view threshold applies to CSOM exactly as it does to the browser. A query that would examine more than 5,000 rows fails regardless of how you issue it.

The answer is the same as it is in the interface: filter on an indexed column, and page.

var query = new CamlQuery
{
    ViewXml = @"<View>
                  <Query>
                    <Where>
                      <Eq><FieldRef Name='Status' /><Value Type='Text'>Approved</Value></Eq>
                    </Where>
                  </Query>
                  <RowLimit>500</RowLimit>
                </View>"
};

do
{
    ListItemCollection items = invoices.GetItems(query);
    context.Load(items);
    context.ExecuteQuery();

    foreach (ListItem item in items)
    {
        Console.WriteLine(item["Title"]);
    }

    // ListItemCollectionPosition carries the paging state. Constructing your
    // own $skip-style paging is where filtered queries start returning wrong
    // results.
    query.ListItemCollectionPosition = items.ListItemCollectionPosition;
}
while (query.ListItemCollectionPosition != null);

Status must be an indexed column for this to work on a large list. Adding the index after the list has grown past the threshold can itself fail, so index early.

Authentication after the ACS retirement

This is where older CSOM guidance has gone stale, and following it now produces code that cannot authenticate at all.

Azure Access Control Services was retired for SharePoint Online on 2 April 2026. Any CSOM code that obtained its token from an ACS app registration — the appregnew.aspx and appinv.aspx flow, or a client ID and secret pair used against SharePoint — stopped working on that date.

The supported approach is an application registered in Microsoft Entra ID, authenticating with OAuth 2.0. For unattended work, use a certificate rather than a client secret, and grant Sites.Selected with explicit per-site grants rather than tenant-wide Sites.FullControl.All.

Where the platform offers managed identity — an Azure Function, for instance — use it. There is then no credential to store, rotate or leak.

CSOM, REST or Microsoft Graph?

SituationReach for
New integration, and Graph covers the scenarioMicrosoft Graph
.NET code needing rich SharePoint-specific operationsCSOM
A language with no SharePoint SDKREST
Bulk provisioning and site configurationCSOM, usually via PnP
Cross-workload work spanning Teams, users and filesMicrosoft Graph
Something Graph does not yet exposeCSOM or REST

For new work, evaluate Graph first — it is more consistent and more actively extended. CSOM remains valuable for SharePoint-specific operations Graph has not reached, and for .NET codebases where a typed object model is more productive than raw HTTP.

In practice most administrative scripting is better served by PnP PowerShell, which wraps CSOM and removes the load-and-execute ceremony entirely.

Common mistakes and better practice

  • Calling ExecuteQuery inside a loop. The most common performance problem in CSOM by a wide margin. Accumulate, then execute once.
  • Load(object) with no property list. Retrieves far more than you need. Always name the properties.
  • Reading a property before executing. Produces PropertyOrFieldNotInitializedException, which reads like a null-reference bug and is not.
  • Ignoring the view threshold. It applies to CSOM. Index the filter column and page.
  • Building on ACS authentication. Retired. Use Microsoft Entra ID.
  • Not disposing ClientContext. It holds connections. Use using.
  • Ignoring throttling. SharePoint throttles aggressive clients. Honour Retry-After, back off, and set a descriptive User-Agent so the service can identify your traffic.

Troubleshooting

ErrorCauseResolution
PropertyOrFieldNotInitializedExceptionProperty read before ExecuteQuery, or not included in LoadAdd it to the Load expression and execute first
401 Unauthorized on code that used to workStill authenticating through retired ACSRe-register the application in Microsoft Entra ID
403 Forbidden after a successful connectionAuthenticated, but without rights on that siteCheck site permissions and the granted application scopes
Threshold error on a filtered queryFilter column not indexedIndex the column; keep RowLimit under the threshold
429 Too Many RequestsThrottlingHonour Retry-After; reduce concurrency; batch more
Code is correct but very slowRound trip per operationBatch loads and execute once

Frequently asked questions

Does CSOM still work with SharePoint Online?

Yes. What changed is authentication: it must go through Microsoft Entra ID now that Azure ACS is retired.

Is CSOM the same as the Server-Side Object Model?

No. SSOM runs on a SharePoint server and only exists on-premises. CSOM runs remotely over HTTP and works against both SharePoint Online and Server.

Why does my property throw before ExecuteQuery?

Because nothing has been fetched. CSOM builds a request locally; the object is empty until the request is dispatched and the response applied.

Should new projects use CSOM or Graph?

Graph first where it covers the scenario. CSOM remains the right tool for SharePoint-specific operations Graph does not yet expose, and for .NET code that benefits from a typed model.

Does the 5,000 item threshold apply?

Yes. It is a platform limit, not a browser limit. Index the columns you filter on and page the results.

Where the effort pays off

Most CSOM performance problems come down to two habits: naming the properties you load, and batching before you execute. Getting those right typically changes an integration's runtime by an order of magnitude, and neither costs anything but attention.

The one thing that is no longer optional is authentication. Code written against ACS is not slow — it does not run at all. If you have CSOM in production that has not been touched since before April 2026, that is the first thing to check.

Share