Skip to content

Cesivi Server - Real-World Examples

Version: 2.0 Last Updated: 2026-03-28


Table of Contents

  1. Getting Started
  2. Setting Up a Mock SharePoint Site
  3. Creating and Populating a Document Library
  4. Working with List Items and Metadata
  5. Content Type Creation and Inheritance
  6. Enterprise Search with Lucene.NET
  7. User Authentication (NTLM, OAuth 2.0)
  8. Batch Operations for Large Datasets
  9. Remote Event Receivers
  10. PnP PowerShell Usage Patterns
  11. CSOM Client Programming
  12. OData Queries - Advanced Filtering

1. Getting Started

Starting the Server

PowerShell:

# Navigate to server directory
cd Cesivi.Server

# Start the server
dotnet run

# Expected output:
# Now listening on: http://localhost:5000
# Application started. Press Ctrl+C to shut down.

Docker:

# Pull and run the container
docker run -d -p 5000:5000 -v ./MockData:/app/MockData cesivi/server:latest

# Verify it's running
curl http://localhost:5000/_health
# Expected: {"status":"Healthy"}

Quick Connectivity Test

REST API:

curl -X GET "http://localhost:5000/_api/web" \
  -H "Accept: application/json" \
  -u "admin:password"

Expected Response:

{
  "d": {
    "Title": "Default Site",
    "Url": "http://localhost:5000/Default/RootSite",
    "Id": "12345678-1234-1234-1234-123456789012"
  }
}


2. Setting Up a Mock SharePoint Site

Complete Site Setup (C#)

using Microsoft.SharePoint.Client;
using System.Net;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

// 1. Update web properties
Web web = context.Web;
context.Load(web);
context.ExecuteQuery();

web.Title = "Team Collaboration Site";
web.Description = "Central hub for team documents and tasks";
web.Update();
context.ExecuteQuery();

// 2. Create standard lists
string[] standardLists = { "Team Documents", "Project Tasks", "Announcements" };
int[] templates = { 101, 107, 104 }; // DocumentLibrary, Tasks, Announcements

for (int i = 0; i < standardLists.Length; i++)
{
    var listInfo = new ListCreationInformation
    {
        Title = standardLists[i],
        TemplateType = templates[i]
    };

    List list = web.Lists.Add(listInfo);
    context.Load(list);
}
context.ExecuteQuery();

// 3. Verify setup
context.Load(web.Lists);
context.ExecuteQuery();

Console.WriteLine($"Site '{web.Title}' configured with {web.Lists.Count} lists");
foreach (List list in web.Lists)
{
    Console.WriteLine($"  - {list.Title} (Template: {list.BaseTemplate})");
}

Expected Output:

Site 'Team Collaboration Site' configured with 4 lists
  - Team Documents (Template: 101)
  - Project Tasks (Template: 107)
  - Announcements (Template: 104)
  - Default List (Template: 100)

Complete Site Setup (PowerShell)

# Connect to Cesivi Server
$cred = Get-Credential -Message "Enter credentials for Cesivi"
Connect-PnPOnline -Url "http://localhost:5000/Default/RootSite" -Credentials $cred

# Update site title
Set-PnPWeb -Title "Team Collaboration Site" -Description "Central hub for team documents and tasks"

# Create standard lists
New-PnPList -Title "Team Documents" -Template DocumentLibrary
New-PnPList -Title "Project Tasks" -Template Tasks
New-PnPList -Title "Announcements" -Template Announcements

# Add custom columns to tasks list
Add-PnPField -List "Project Tasks" -DisplayName "Project Name" -InternalName "ProjectName" -Type Text
Add-PnPField -List "Project Tasks" -DisplayName "Estimated Hours" -InternalName "EstimatedHours" -Type Number
Add-PnPField -List "Project Tasks" -DisplayName "Completion Date" -InternalName "CompletionDate" -Type DateTime

# Verify setup
Get-PnPList | Select-Object Title, BaseTemplate, ItemCount | Format-Table

3. Creating and Populating a Document Library

Complete Document Library Workflow (C#)

using Microsoft.SharePoint.Client;
using System.Text;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

Web web = context.Web;

// 1. Create document library
var libraryInfo = new ListCreationInformation
{
    Title = "Project Documents",
    TemplateType = (int)ListTemplateType.DocumentLibrary,
    Description = "Project documentation and deliverables"
};

List library = web.Lists.Add(libraryInfo);
library.EnableVersioning = true;
library.EnableMinorVersions = true;
library.MajorVersionLimit = 50;
library.Update();
context.ExecuteQuery();

// 2. Add custom columns
Field projectField = library.Fields.AddFieldAsXml(
    "<Field Type='Text' DisplayName='Project' Name='Project' Required='TRUE'/>",
    true, AddFieldOptions.DefaultValue);

Field statusField = library.Fields.AddFieldAsXml(
    "<Field Type='Choice' DisplayName='Document Status' Name='DocStatus'>" +
    "<CHOICES><CHOICE>Draft</CHOICE><CHOICE>Review</CHOICE><CHOICE>Approved</CHOICE></CHOICES>" +
    "</Field>",
    true, AddFieldOptions.DefaultValue);

context.ExecuteQuery();

// 3. Create folder structure
context.Load(library.RootFolder);
context.ExecuteQuery();

string[] folders = { "Requirements", "Design", "Testing", "Deliverables" };
foreach (string folderName in folders)
{
    library.RootFolder.Folders.Add(folderName);
}
context.ExecuteQuery();

// 4. Upload sample documents
var documents = new[]
{
    new { Name = "Requirements.docx", Folder = "Requirements", Content = "Requirements Document" },
    new { Name = "Architecture.pdf", Folder = "Design", Content = "Architecture Design" },
    new { Name = "TestPlan.docx", Folder = "Testing", Content = "Test Plan" },
    new { Name = "Release_v1.0.zip", Folder = "Deliverables", Content = "Release Package" }
};

foreach (var doc in documents)
{
    var fileInfo = new FileCreationInformation
    {
        Content = Encoding.UTF8.GetBytes(doc.Content),
        Url = $"{doc.Folder}/{doc.Name}",
        Overwrite = true
    };

    File uploadedFile = library.RootFolder.Files.Add(fileInfo);
    context.Load(uploadedFile, f => f.ListItemAllFields);
    context.ExecuteQuery();

    // Set metadata
    uploadedFile.ListItemAllFields["Project"] = "Website Redesign";
    uploadedFile.ListItemAllFields["DocStatus"] = "Draft";
    uploadedFile.ListItemAllFields.Update();
}
context.ExecuteQuery();

Console.WriteLine($"Document library '{library.Title}' created with {documents.Length} documents");

Document Library Setup (REST API)

# Create library
curl -X POST "http://localhost:5000/_api/web/lists" \
  -H "Content-Type: application/json;odata=verbose" \
  -H "Accept: application/json;odata=verbose" \
  -u "admin:password" \
  -d '{
    "__metadata": { "type": "SP.List" },
    "BaseTemplate": 101,
    "Title": "Project Documents",
    "Description": "Project documentation"
  }'

# Create folder
curl -X POST "http://localhost:5000/_api/web/folders" \
  -H "Content-Type: application/json;odata=verbose" \
  -u "admin:password" \
  -d '{
    "__metadata": { "type": "SP.Folder" },
    "ServerRelativeUrl": "/Default/RootSite/Project Documents/Requirements"
  }'

# Upload document (base64 encoded)
curl -X POST "http://localhost:5000/_api/web/GetFolderByServerRelativeUrl('/Default/RootSite/Project Documents')/Files/add(url='document.txt',overwrite=true)" \
  -H "Content-Type: application/octet-stream" \
  -u "admin:password" \
  --data-binary "@document.txt"

4. Working with List Items and Metadata

Complete CRUD Operations (C#)

using Microsoft.SharePoint.Client;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

Web web = context.Web;

// 1. Create list with custom columns
var listInfo = new ListCreationInformation
{
    Title = "Customer Orders",
    TemplateType = (int)ListTemplateType.GenericList
};

List list = web.Lists.Add(listInfo);
context.ExecuteQuery();

// Add columns
list.Fields.AddFieldAsXml(
    "<Field Type='Text' DisplayName='Customer Name' Name='CustomerName' Required='TRUE'/>",
    true, AddFieldOptions.DefaultValue);
list.Fields.AddFieldAsXml(
    "<Field Type='Number' DisplayName='Order Amount' Name='OrderAmount' Min='0' Decimals='2'/>",
    true, AddFieldOptions.DefaultValue);
list.Fields.AddFieldAsXml(
    "<Field Type='Choice' DisplayName='Status' Name='OrderStatus'>" +
    "<CHOICES><CHOICE>Pending</CHOICE><CHOICE>Processing</CHOICE><CHOICE>Shipped</CHOICE><CHOICE>Delivered</CHOICE></CHOICES>" +
    "</Field>",
    true, AddFieldOptions.DefaultValue);
list.Fields.AddFieldAsXml(
    "<Field Type='DateTime' DisplayName='Order Date' Name='OrderDate'/>",
    true, AddFieldOptions.DefaultValue);
context.ExecuteQuery();

// 2. CREATE - Add items
var orders = new[]
{
    new { Title = "ORD-001", Customer = "Acme Corp", Amount = 1500.00m, Status = "Processing", Date = DateTime.Now.AddDays(-5) },
    new { Title = "ORD-002", Customer = "Globex Inc", Amount = 3200.50m, Status = "Shipped", Date = DateTime.Now.AddDays(-3) },
    new { Title = "ORD-003", Customer = "Initech", Amount = 750.00m, Status = "Pending", Date = DateTime.Now.AddDays(-1) },
    new { Title = "ORD-004", Customer = "Umbrella Corp", Amount = 5000.00m, Status = "Delivered", Date = DateTime.Now.AddDays(-10) },
    new { Title = "ORD-005", Customer = "Acme Corp", Amount = 2100.00m, Status = "Processing", Date = DateTime.Now }
};

foreach (var order in orders)
{
    ListItem item = list.AddItem(new ListItemCreationInformation());
    item["Title"] = order.Title;
    item["CustomerName"] = order.Customer;
    item["OrderAmount"] = order.Amount;
    item["OrderStatus"] = order.Status;
    item["OrderDate"] = order.Date;
    item.Update();
}
context.ExecuteQuery();
Console.WriteLine($"Created {orders.Length} orders");

// 3. READ - Query items with CAML
CamlQuery query = new CamlQuery
{
    ViewXml = @"
    <View>
        <Query>
            <Where>
                <And>
                    <Eq>
                        <FieldRef Name='CustomerName' />
                        <Value Type='Text'>Acme Corp</Value>
                    </Eq>
                    <Gt>
                        <FieldRef Name='OrderAmount' />
                        <Value Type='Number'>1000</Value>
                    </Gt>
                </And>
            </Where>
            <OrderBy>
                <FieldRef Name='OrderDate' Ascending='FALSE' />
            </OrderBy>
        </Query>
    </View>"
};

ListItemCollection items = list.GetItems(query);
context.Load(items);
context.ExecuteQuery();

Console.WriteLine($"\nAcme Corp orders over $1000:");
foreach (ListItem item in items)
{
    Console.WriteLine($"  {item["Title"]}: ${item["OrderAmount"]} ({item["OrderStatus"]})");
}

// 4. UPDATE - Update item status
ListItem itemToUpdate = list.GetItemById(3); // ORD-003
itemToUpdate["OrderStatus"] = "Processing";
itemToUpdate["Title"] = "ORD-003-UPDATED";
itemToUpdate.Update();
context.ExecuteQuery();
Console.WriteLine("\nUpdated ORD-003 to Processing");

// 5. DELETE - Delete an item
ListItem itemToDelete = list.GetItemById(4); // ORD-004
itemToDelete.DeleteObject();
context.ExecuteQuery();
Console.WriteLine("Deleted ORD-004");

// 6. Verify final state
context.Load(list, l => l.ItemCount);
context.ExecuteQuery();
Console.WriteLine($"\nFinal item count: {list.ItemCount}");

List Item Operations (REST API)

# Create item
curl -X POST "http://localhost:5000/_api/web/lists/getbytitle('Customer Orders')/items" \
  -H "Content-Type: application/json;odata=verbose" \
  -H "Accept: application/json;odata=verbose" \
  -u "admin:password" \
  -d '{
    "__metadata": { "type": "SP.Data.CustomerOrdersListItem" },
    "Title": "ORD-006",
    "CustomerName": "New Customer",
    "OrderAmount": 999.99,
    "OrderStatus": "Pending"
  }'

# Read items with filter
curl -X GET "http://localhost:5000/_api/web/lists/getbytitle('Customer%20Orders')/items?\$filter=OrderAmount%20gt%201000&\$orderby=OrderAmount%20desc" \
  -H "Accept: application/json;odata=verbose" \
  -u "admin:password"

# Update item
curl -X MERGE "http://localhost:5000/_api/web/lists/getbytitle('Customer Orders')/items(1)" \
  -H "Content-Type: application/json;odata=verbose" \
  -H "IF-MATCH: *" \
  -u "admin:password" \
  -d '{
    "__metadata": { "type": "SP.Data.CustomerOrdersListItem" },
    "OrderStatus": "Shipped"
  }'

# Delete item
curl -X DELETE "http://localhost:5000/_api/web/lists/getbytitle('Customer Orders')/items(2)" \
  -H "IF-MATCH: *" \
  -u "admin:password"

5. Content Type Creation and Inheritance

Multi-Level Content Type Hierarchy (C#)

using Microsoft.SharePoint.Client;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

Web web = context.Web;

// 1. Get built-in Document content type (0x0101)
ContentType documentCT = web.ContentTypes.GetById("0x0101");
context.Load(documentCT, ct => ct.Id, ct => ct.Name, ct => ct.Fields);
context.ExecuteQuery();
Console.WriteLine($"Base: {documentCT.Name} ({documentCT.Id.StringValue})");

// 2. Create Level 1: ProjectDocument (inherits from Document)
var projectDocInfo = new ContentTypeCreationInformation
{
    Name = "Project Document",
    Description = "Base content type for all project documents",
    ParentContentType = documentCT,
    Group = "Project Content Types"
};

ContentType projectDocCT = web.ContentTypes.Add(projectDocInfo);
context.Load(projectDocCT, ct => ct.Id, ct => ct.Name);
context.ExecuteQuery();

// Add fields to ProjectDocument
projectDocCT.FieldLinks.Add(new FieldLinkCreationInformation
{
    Field = web.Fields.GetByInternalNameOrTitle("Project")
});
projectDocCT.Update(true);
context.ExecuteQuery();
Console.WriteLine($"Level 1: {projectDocCT.Name} ({projectDocCT.Id.StringValue})");

// 3. Create Level 2: TechnicalSpec (inherits from ProjectDocument)
var techSpecInfo = new ContentTypeCreationInformation
{
    Name = "Technical Specification",
    Description = "Technical specification documents",
    ParentContentType = projectDocCT,
    Group = "Project Content Types"
};

ContentType techSpecCT = web.ContentTypes.Add(techSpecInfo);
context.Load(techSpecCT, ct => ct.Id, ct => ct.Name, ct => ct.Fields);
context.ExecuteQuery();
Console.WriteLine($"Level 2: {techSpecCT.Name} ({techSpecCT.Id.StringValue})");

// 4. Create Level 3: APISpecification (inherits from TechnicalSpec)
var apiSpecInfo = new ContentTypeCreationInformation
{
    Name = "API Specification",
    Description = "REST/SOAP API specification documents",
    ParentContentType = techSpecCT,
    Group = "Project Content Types"
};

ContentType apiSpecCT = web.ContentTypes.Add(apiSpecInfo);
context.Load(apiSpecCT, ct => ct.Id, ct => ct.Name, ct => ct.Fields);
context.ExecuteQuery();
Console.WriteLine($"Level 3: {apiSpecCT.Name} ({apiSpecCT.Id.StringValue})");

// 5. Verify inheritance chain
Console.WriteLine($"\nInheritance verified:");
Console.WriteLine($"  API Specification inherits {apiSpecCT.Fields.Count} fields from ancestors");

// 6. Add content type to library
List library = web.Lists.GetByTitle("Project Documents");
library.ContentTypesEnabled = true;
library.Update();
context.ExecuteQuery();

library.ContentTypes.AddExistingContentType(apiSpecCT);
context.Load(library.ContentTypes);
context.ExecuteQuery();

Console.WriteLine($"\nLibrary now has {library.ContentTypes.Count} content types");

Expected Output:

Base: Document (0x0101)
Level 1: Project Document (0x010100abc123...)
Level 2: Technical Specification (0x010100abc123def456...)
Level 3: API Specification (0x010100abc123def456ghi789...)

Inheritance verified:
  API Specification inherits 15 fields from ancestors

Library now has 4 content types


6. Enterprise Search with Lucene.NET

Search Query Examples (REST API)

# Basic keyword search
curl -X GET "http://localhost:5000/_api/search/query?querytext='project%20documentation'&rowlimit=10" \
  -H "Accept: application/json" \
  -u "admin:password"

# Search with refiners
curl -X GET "http://localhost:5000/_api/search/query?querytext='*'&refiners='FileType,Author'&rowlimit=20" \
  -H "Accept: application/json" \
  -u "admin:password"

# Search specific content type
curl -X GET "http://localhost:5000/_api/search/query?querytext='ContentType:Document'&rowlimit=50" \
  -H "Accept: application/json" \
  -u "admin:password"

# Search with date range
curl -X GET "http://localhost:5000/_api/search/query?querytext='Created:2026-01-01..2026-01-31'&rowlimit=100" \
  -H "Accept: application/json" \
  -u "admin:password"

Search Implementation (C#)

using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;

var baseUrl = "http://localhost:5000";
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Basic",
        Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:password")));

// 1. Full-text search
var searchUrl = $"{baseUrl}/_api/search/query?querytext='project documentation'&rowlimit=20";
var response = await client.GetAsync(searchUrl);
var content = await response.Content.ReadAsStringAsync();

var results = JsonDocument.Parse(content);
var rows = results.RootElement
    .GetProperty("d")
    .GetProperty("query")
    .GetProperty("PrimaryQueryResult")
    .GetProperty("RelevantResults")
    .GetProperty("Table")
    .GetProperty("Rows")
    .GetProperty("results");

Console.WriteLine("Search Results:");
foreach (var row in rows.EnumerateArray())
{
    var cells = row.GetProperty("Cells").GetProperty("results");
    string title = "", path = "";

    foreach (var cell in cells.EnumerateArray())
    {
        var key = cell.GetProperty("Key").GetString();
        var value = cell.GetProperty("Value").GetString();

        if (key == "Title") title = value;
        if (key == "Path") path = value;
    }

    Console.WriteLine($"  - {title}");
    Console.WriteLine($"    Path: {path}");
}

// 2. Search with advanced options
searchUrl = $"{baseUrl}/_api/search/query?" +
    "querytext='status:active AND priority:high'" +
    "&selectproperties='Title,Path,Author,Created'" +
    "&sortlist='Created:descending'" +
    "&rowlimit=10";

response = await client.GetAsync(searchUrl);
Console.WriteLine($"\nAdvanced search returned: {response.StatusCode}");

7. User Authentication (NTLM, OAuth 2.0)

OAuth 2.0 / Azure AD Mock Authentication

# 1. Get access token from Azure AD mock
$tokenResponse = Invoke-RestMethod -Method Post `
    -Uri "http://localhost:5000/oauth2/v2.0/token" `
    -ContentType "application/x-www-form-urlencoded" `
    -Body @{
        grant_type    = "client_credentials"
        client_id     = "12345678-1234-1234-1234-123456789012"
        client_secret = "mock-secret"
        scope         = "http://localhost:5000/.default"
    }

$accessToken = $tokenResponse.access_token
Write-Host "Access token obtained: $($accessToken.Substring(0, 50))..."

# 2. Use token with REST API
$headers = @{
    "Authorization" = "Bearer $accessToken"
    "Accept"        = "application/json;odata=verbose"
}

$webInfo = Invoke-RestMethod -Uri "http://localhost:5000/_api/web" -Headers $headers
Write-Host "Connected to: $($webInfo.d.Title)"

# 3. Use token with PnP PowerShell
$secureToken = ConvertTo-SecureString $accessToken -AsPlainText -Force
Connect-PnPOnline -Url "http://localhost:5000/Default/RootSite" -AccessToken $secureToken

Get-PnPWeb | Select-Object Title, Url
Get-PnPList | Select-Object Title, ItemCount

NTLM Authentication

using Microsoft.SharePoint.Client;
using System.Net;

// Windows Integrated Authentication (NTLM)
var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    // Option 1: Current Windows user
    Credentials = CredentialCache.DefaultNetworkCredentials
};

// Option 2: Specific domain credentials
context.Credentials = new NetworkCredential(
    "username",
    "password",
    "DOMAIN"
);

Web web = context.Web;
context.Load(web);
context.ExecuteQuery();

Console.WriteLine($"Connected as: {context.Credentials}");
Console.WriteLine($"Site: {web.Title}");

Basic Authentication

# Using curl with Basic auth
curl -X GET "http://localhost:5000/_api/web" \
  -u "admin:password" \
  -H "Accept: application/json"

# Using PowerShell
$cred = Get-Credential
Invoke-RestMethod -Uri "http://localhost:5000/_api/web" `
  -Credential $cred `
  -Headers @{ Accept = "application/json" }

8. Batch Operations for Large Datasets

Optimized Batch Item Creation (C#)

using Microsoft.SharePoint.Client;
using System.Diagnostics;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

Web web = context.Web;
List list = web.Lists.GetByTitle("Performance Test");
context.Load(list);
context.ExecuteQuery();

// Batch configuration - optimized for Cesivi Server
const int BATCH_SIZE = 100;      // Items per batch (optimal: 50-100)
const int TOTAL_ITEMS = 1000;    // Total items to create

var stopwatch = Stopwatch.StartNew();
int created = 0;

Console.WriteLine($"Creating {TOTAL_ITEMS} items in batches of {BATCH_SIZE}...");

while (created < TOTAL_ITEMS)
{
    var batchStart = Stopwatch.StartNew();

    // Add items to batch
    for (int i = 0; i < BATCH_SIZE && created < TOTAL_ITEMS; i++)
    {
        ListItem item = list.AddItem(new ListItemCreationInformation());
        item["Title"] = $"Item-{created + 1:D5}";
        item["Status"] = created % 3 == 0 ? "Active" : created % 3 == 1 ? "Pending" : "Closed";
        item["Priority"] = (created % 10) + 1;
        item["Description"] = $"Auto-generated item {created + 1}";
        item.Update();
        created++;
    }

    // Execute batch
    context.ExecuteQuery();

    var batchTime = batchStart.ElapsedMilliseconds;
    var itemsPerSec = BATCH_SIZE / (batchTime / 1000.0);
    Console.WriteLine($"  Batch complete: {created}/{TOTAL_ITEMS} items ({batchTime}ms, {itemsPerSec:F1} items/sec)");
}

stopwatch.Stop();
var totalSeconds = stopwatch.Elapsed.TotalSeconds;
var avgPerItem = stopwatch.ElapsedMilliseconds / (double)TOTAL_ITEMS;

Console.WriteLine($"\nCompleted:");
Console.WriteLine($"  Total time: {totalSeconds:F1} seconds");
Console.WriteLine($"  Average: {avgPerItem:F1}ms per item");
Console.WriteLine($"  Throughput: {TOTAL_ITEMS / totalSeconds:F1} items/second");

Expected Performance (after PLAN-179 optimization):

Creating 1000 items in batches of 100...
  Batch complete: 100/1000 items (1520ms, 65.8 items/sec)
  Batch complete: 200/1000 items (1498ms, 66.8 items/sec)
  ...
  Batch complete: 1000/1000 items (1534ms, 65.2 items/sec)

Completed:
  Total time: 15.3 seconds
  Average: 15.3ms per item
  Throughput: 65.4 items/second

Batch Operations Best Practices

// DO: Use appropriate batch sizes
const int OPTIMAL_BATCH_SIZE = 100;  // Best balance of speed vs memory

// DO: Reuse context across batches
using var context = new ClientContext(siteUrl);
context.Credentials = credentials;

// DON'T: Create new context per batch
// This causes connection overhead

// DO: Handle errors gracefully
try
{
    context.ExecuteQuery();
}
catch (ServerException ex)
{
    Console.WriteLine($"Batch failed at item {created}: {ex.Message}");
    // Retry or log and continue
}

// DO: Monitor memory usage for very large batches
if (created % 1000 == 0)
{
    GC.Collect();  // Prevent memory buildup
}

9. Remote Event Receivers

Event Receiver Registration (C#)

using Microsoft.SharePoint.Client;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

Web web = context.Web;
List list = web.Lists.GetByTitle("Customer Orders");
context.Load(list);
context.ExecuteQuery();

// Register ItemAdded event receiver
var receiverInfo = new EventReceiverDefinitionCreationInformation
{
    EventType = EventReceiverType.ItemAdded,
    ReceiverName = "OrderNotification",
    ReceiverUrl = "https://your-service.com/api/events/order-added",
    SequenceNumber = 1000,
    Synchronization = EventReceiverSynchronization.Asynchronous
};

list.EventReceivers.Add(receiverInfo);
context.ExecuteQuery();

Console.WriteLine("Event receiver registered successfully");

// List all event receivers
context.Load(list.EventReceivers);
context.ExecuteQuery();

Console.WriteLine("\nRegistered Event Receivers:");
foreach (var receiver in list.EventReceivers)
{
    Console.WriteLine($"  - {receiver.ReceiverName} ({receiver.EventType})");
    Console.WriteLine($"    URL: {receiver.ReceiverUrl}");
}

Event Receiver Service Example (ASP.NET Core)

// EventController.cs - Your webhook endpoint
[ApiController]
[Route("api/events")]
public class EventController : ControllerBase
{
    [HttpPost("order-added")]
    public async Task<IActionResult> HandleOrderAdded([FromBody] SPRemoteEventProperties properties)
    {
        // Extract event information
        var itemId = properties.ItemEventProperties.ListItemId;
        var listTitle = properties.ItemEventProperties.ListTitle;
        var afterProperties = properties.ItemEventProperties.AfterProperties;

        Console.WriteLine($"New order added: Item {itemId} in {listTitle}");

        // Process the event (e.g., send notification, update external system)
        await SendOrderNotificationAsync(afterProperties["Title"], afterProperties["CustomerName"]);

        // Return success
        return Ok(new SPRemoteEventResult { Status = SPRemoteEventServiceStatus.Continue });
    }

    private async Task SendOrderNotificationAsync(string orderId, string customer)
    {
        // Your notification logic here
        Console.WriteLine($"Notification sent for order {orderId} from {customer}");
    }
}

10. PnP PowerShell Usage Patterns

Complete Site Provisioning

# Connect with OAuth 2.0
$token = Get-CesiviAccessToken -ClientId "your-client-id" -ClientSecret "your-secret"
$secureToken = ConvertTo-SecureString $token -AsPlainText -Force
Connect-PnPOnline -Url "http://localhost:5000/Default/RootSite" -AccessToken $secureToken

# Or connect with credentials
$cred = Get-Credential
Connect-PnPOnline -Url "http://localhost:5000/Default/RootSite" -Credentials $cred

# 1. Create site structure
$lists = @(
    @{ Title = "Projects"; Template = "GenericList" },
    @{ Title = "Documents"; Template = "DocumentLibrary" },
    @{ Title = "Tasks"; Template = "Tasks" },
    @{ Title = "Calendar"; Template = "Events" }
)

foreach ($listDef in $lists) {
    New-PnPList -Title $listDef.Title -Template $listDef.Template -ErrorAction SilentlyContinue
    Write-Host "Created list: $($listDef.Title)"
}

# 2. Add custom site columns
$columns = @(
    @{ Name = "ProjectCode"; Type = "Text"; Group = "Custom Columns" },
    @{ Name = "Department"; Type = "Choice"; Choices = @("IT", "HR", "Finance", "Operations") },
    @{ Name = "StartDate"; Type = "DateTime"; Group = "Custom Columns" },
    @{ Name = "Budget"; Type = "Currency"; Group = "Custom Columns" }
)

foreach ($col in $columns) {
    if ($col.Type -eq "Choice") {
        Add-PnPField -DisplayName $col.Name -InternalName $col.Name -Type $col.Type -Choices $col.Choices -Group "Custom Columns"
    } else {
        Add-PnPField -DisplayName $col.Name -InternalName $col.Name -Type $col.Type -Group "Custom Columns"
    }
    Write-Host "Created column: $($col.Name)"
}

# 3. Add columns to lists
Add-PnPField -List "Projects" -Field "ProjectCode"
Add-PnPField -List "Projects" -Field "Department"
Add-PnPField -List "Projects" -Field "StartDate"
Add-PnPField -List "Projects" -Field "Budget"

# 4. Create content type
$ct = Add-PnPContentType -Name "Project Item" -Description "Content type for projects" -Group "Custom Content Types" -ParentContentType "Item"
Add-PnPFieldToContentType -Field "ProjectCode" -ContentType "Project Item"
Add-PnPFieldToContentType -Field "Department" -ContentType "Project Item"

# 5. Populate with sample data
$projects = @(
    @{ Title = "Website Redesign"; ProjectCode = "PRJ-001"; Department = "IT"; Budget = 50000 },
    @{ Title = "Employee Training"; ProjectCode = "PRJ-002"; Department = "HR"; Budget = 15000 },
    @{ Title = "Budget Analysis"; ProjectCode = "PRJ-003"; Department = "Finance"; Budget = 5000 }
)

foreach ($proj in $projects) {
    Add-PnPListItem -List "Projects" -Values $proj
    Write-Host "Added project: $($proj.Title)"
}

# 6. Verify setup
Write-Host "`n=== Site Configuration ==="
Get-PnPWeb | Format-Table Title, Url
Get-PnPList | Format-Table Title, BaseTemplate, ItemCount
Get-PnPField -Group "Custom Columns" | Format-Table Title, TypeAsString

Disconnect-PnPOnline

11. CSOM Client Programming

Complete Workflow with Lambda Expressions

using Microsoft.SharePoint.Client;
using System.Linq.Expressions;

var siteUrl = "http://localhost:5000/Default/RootSite";
using var context = new ClientContext(siteUrl)
{
    Credentials = new NetworkCredential("admin", "password")
};

// 1. Load web with specific properties (lambda expressions work!)
Web web = context.Web;
context.Load(web,
    w => w.Title,
    w => w.Description,
    w => w.Url,
    w => w.Created,
    w => w.CurrentUser);
context.ExecuteQuery();

Console.WriteLine($"Site: {web.Title}");
Console.WriteLine($"URL: {web.Url}");
Console.WriteLine($"Created: {web.Created}");
Console.WriteLine($"Current User: {web.CurrentUser.Title}");

// 2. Load lists with Include() (now fully supported!)
context.Load(web.Lists,
    lists => lists.Include(
        l => l.Title,
        l => l.Description,
        l => l.ItemCount,
        l => l.BaseTemplate,
        l => l.Created
    ));
context.ExecuteQuery();

Console.WriteLine($"\nLists ({web.Lists.Count}):");
foreach (List list in web.Lists)
{
    if (!list.Hidden)
    {
        Console.WriteLine($"  - {list.Title}: {list.ItemCount} items (Template: {list.BaseTemplate})");
    }
}

// 3. Complex nested includes
List taskList = web.Lists.GetByTitle("Project Tasks");
context.Load(taskList,
    l => l.Title,
    l => l.Fields.Include(
        f => f.Title,
        f => f.InternalName,
        f => f.TypeAsString,
        f => f.Required
    ),
    l => l.ContentTypes.Include(
        ct => ct.Name,
        ct => ct.Description
    ));
context.ExecuteQuery();

Console.WriteLine($"\n{taskList.Title} Structure:");
Console.WriteLine("  Fields:");
foreach (Field field in taskList.Fields)
{
    if (!field.Hidden)
    {
        Console.WriteLine($"    - {field.Title} ({field.TypeAsString}){(field.Required ? " *" : "")}");
    }
}

Console.WriteLine("  Content Types:");
foreach (ContentType ct in taskList.ContentTypes)
{
    Console.WriteLine($"    - {ct.Name}: {ct.Description}");
}

12. OData Queries - Advanced Filtering

Complex Filter Examples

# 1. String functions - case-insensitive search
curl "http://localhost:5000/_api/web/lists/getbytitle('Tasks')/items?\$filter=startswith(tolower(Title),'project')"

# 2. Date functions - items created in 2026
curl "http://localhost:5000/_api/web/lists/getbytitle('Tasks')/items?\$filter=year(Created) eq 2026"

# 3. Combined date filters
curl "http://localhost:5000/_api/web/lists/getbytitle('Tasks')/items?\$filter=month(DueDate) eq 1 and year(DueDate) eq 2026"

# 4. DateTime literals
curl "http://localhost:5000/_api/web/lists/getbytitle('Tasks')/items?\$filter=Created gt datetime'2026-01-01T00:00:00Z'"

# 5. Complex boolean logic
curl "http://localhost:5000/_api/web/lists/getbytitle('Orders')/items?\$filter=(Status eq 'Pending' or Status eq 'Processing') and Amount gt 1000 and startswith(CustomerName,'A')"

# 6. Nested $expand with query options
curl "http://localhost:5000/_api/web?\$expand=Lists(\$select=Title,ItemCount;\$filter=Hidden eq false;\$orderby=Title;\$top=5)"

# 7. Multiple $expand with different options
curl "http://localhost:5000/_api/web?\$expand=Lists(\$select=Title),ContentTypes(\$select=Name,Description)"

OData Query Builder (C#)

using System.Net.Http;
using System.Web;

public class ODataQueryBuilder
{
    private readonly string _baseUrl;
    private readonly List<string> _filters = new();
    private readonly List<string> _selects = new();
    private readonly List<string> _expands = new();
    private string _orderBy;
    private int? _top;
    private int? _skip;

    public ODataQueryBuilder(string baseUrl) => _baseUrl = baseUrl;

    public ODataQueryBuilder Filter(string filter)
    {
        _filters.Add(filter);
        return this;
    }

    public ODataQueryBuilder Select(params string[] fields)
    {
        _selects.AddRange(fields);
        return this;
    }

    public ODataQueryBuilder Expand(string property, string options = null)
    {
        _expands.Add(options != null ? $"{property}({options})" : property);
        return this;
    }

    public ODataQueryBuilder OrderBy(string field, bool descending = false)
    {
        _orderBy = descending ? $"{field} desc" : field;
        return this;
    }

    public ODataQueryBuilder Top(int count)
    {
        _top = count;
        return this;
    }

    public ODataQueryBuilder Skip(int count)
    {
        _skip = count;
        return this;
    }

    public string Build()
    {
        var parts = new List<string>();

        if (_filters.Any())
            parts.Add($"$filter={HttpUtility.UrlEncode(string.Join(" and ", _filters))}");
        if (_selects.Any())
            parts.Add($"$select={string.Join(",", _selects)}");
        if (_expands.Any())
            parts.Add($"$expand={string.Join(",", _expands)}");
        if (_orderBy != null)
            parts.Add($"$orderby={_orderBy}");
        if (_top.HasValue)
            parts.Add($"$top={_top}");
        if (_skip.HasValue)
            parts.Add($"$skip={_skip}");

        return parts.Any() ? $"{_baseUrl}?{string.Join("&", parts)}" : _baseUrl;
    }
}

// Usage example
var query = new ODataQueryBuilder("http://localhost:5000/_api/web/lists/getbytitle('Orders')/items")
    .Filter("Status eq 'Active'")
    .Filter("Amount gt 1000")
    .Select("Id", "Title", "Amount", "CustomerName")
    .OrderBy("Amount", descending: true)
    .Top(20)
    .Expand("Author", "$select=Title,Email")
    .Build();

Console.WriteLine(query);
// Output: http://localhost:5000/_api/web/lists/getbytitle('Orders')/items?
//         $filter=Status%20eq%20'Active'%20and%20Amount%20gt%201000&
//         $select=Id,Title,Amount,CustomerName&
//         $orderby=Amount desc&
//         $top=20&
//         $expand=Author($select=Title,Email)

Quick Reference Card

Common Endpoints

Operation REST Endpoint Method
Get Web /_api/web GET
Get Lists /_api/web/lists GET
Get List /_api/web/lists/getbytitle('{title}') GET
Create List /_api/web/lists POST
Get Items /_api/web/lists/getbytitle('{title}')/items GET
Create Item /_api/web/lists/getbytitle('{title}')/items POST
Update Item /_api/web/lists/getbytitle('{title}')/items({id}) MERGE
Delete Item /_api/web/lists/getbytitle('{title}')/items({id}) DELETE
Search /_api/search/query?querytext='{query}' GET
Get Token /oauth2/v2.0/token POST

OData Operators

Operator Example
eq $filter=Status eq 'Active'
ne $filter=Status ne 'Closed'
gt, ge $filter=Amount gt 1000
lt, le $filter=Priority le 5
and $filter=Status eq 'Active' and Priority gt 5
or $filter=Status eq 'Active' or Status eq 'Pending'
startswith $filter=startswith(Title,'Project')
substringof $filter=substringof('urgent',Title)
tolower $filter=startswith(tolower(Title),'project')
year/month/day $filter=year(Created) eq 2026

Document Version: 2.0 Last Updated: 2026-03-28 Author: Cesivi Server Team