Cesivi Server - Real-World Examples¶
Version: 2.0 Last Updated: 2026-03-28
Table of Contents¶
- Getting Started
- Setting Up a Mock SharePoint Site
- Creating and Populating a Document Library
- Working with List Items and Metadata
- Content Type Creation and Inheritance
- Enterprise Search with Lucene.NET
- User Authentication (NTLM, OAuth 2.0)
- Batch Operations for Large Datasets
- Remote Event Receivers
- PnP PowerShell Usage Patterns
- CSOM Client Programming
- 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