Customization Migration Guide: Farm Solutions to SPFx¶
Status: Authored 2026-04-30 Audience: SharePoint developers, solution architects, and IT administrators migrating custom SharePoint solutions from on-premises to SharePoint Online. Scope: Farm solutions, sandbox solutions, SharePoint Designer customizations, custom web parts, master pages, and their SPFx/modern equivalents.
1. Customization migration overview¶
SharePoint Online does not support server-side code execution. Farm solutions (WSP files deployed to the server), sandbox solutions with managed code, and SharePoint Designer customizations that rely on server-side features must be migrated to the SharePoint Framework (SPFx), Power Platform, or Azure services.
Migration target by customization type¶
| On-premises customization | Primary SPO target | Alternative target |
|---|---|---|
| Farm solution (web parts) | SPFx web parts | Power Apps embedded in SPO page |
| Farm solution (timer jobs) | Azure Functions (timer trigger) | Power Automate scheduled flows |
| Farm solution (event receivers) | SPFx extensions + webhooks | Power Automate triggers |
| Farm solution (application pages) | SPFx full-page applications | Power Apps standalone app |
| Farm solution (custom service apps) | Azure App Service + Microsoft Graph | Custom API on Azure |
| Sandbox solution (web parts) | SPFx web parts | Power Apps |
| Sandbox solution (event receivers) | SPFx extensions + webhooks | Power Automate |
| SP Designer workflows | Power Automate | See Workflow Migration |
| SP Designer custom pages | Modern pages with SPFx web parts | Communication site pages |
| Custom master pages | Modern theming + SPFx app customizer | JSON themes |
| Custom page layouts | Modern page sections + web parts | SPFx section backgrounds |
| Custom CSS | Modern theming (JSON) | SPFx app customizer for advanced CSS |
| JavaScript injection (CEWP/Script Editor) | SPFx extensions (app customizer) | SPFx web parts |
| Custom web parts (server-side) | SPFx web parts (client-side) | Power Apps |
2. Farm solution assessment¶
Inventory farm solutions¶
Add-PSSnapin Microsoft.SharePoint.PowerShell
# List all farm solutions
Get-SPSolution | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
SolutionId = $_.SolutionId
Deployed = $_.Deployed
DeployedWebApps = ($_.DeployedWebApplications | Select-Object -ExpandProperty Url) -join "; "
ContainsGlobalAssembly = $_.ContainsGlobalAssembly
ContainsWebAppResource = $_.ContainsWebApplicationResource
ContainsCasPolicy = $_.ContainsCasPolicy
LastModified = $_.LastOperationEndTime
}
} | Export-Csv -Path "C:\Migration\farm-solutions.csv" -NoTypeInformation
# List sandbox solutions per site collection
Get-SPSite -Limit All | ForEach-Object {
$site = $_
$_.Solutions | ForEach-Object {
[PSCustomObject]@{
SiteUrl = $site.Url
Name = $_.Name
SolutionId = $_.SolutionId
Status = $_.Status
HasAssembly = $_.HasAssembly
}
}
} | Export-Csv -Path "C:\Migration\sandbox-solutions.csv" -NoTypeInformation
WSP contents analysis¶
# Extract and analyze a WSP file (it is a CAB file)
$wspPath = "C:\Solutions\MyCustomSolution.wsp"
$extractPath = "C:\Solutions\Extracted"
# Extract using expand (WSP is a CAB)
expand $wspPath -F:* $extractPath
# Analyze the manifest
[xml]$manifest = Get-Content "$extractPath\manifest.xml"
# List assemblies
$manifest.Solution.Assemblies.Assembly | ForEach-Object {
[PSCustomObject]@{
Location = $_.Location
DeploymentTarget = $_.DeploymentTarget
}
}
# List features
$manifest.Solution.Features.Feature | ForEach-Object {
[PSCustomObject]@{
Location = $_.Location
}
}
Complexity classification for farm solutions¶
| Complexity | Characteristics | SPFx conversion effort |
|---|---|---|
| Simple | Visual web parts with HTML/JS only, no server-side logic | 1-3 days per web part |
| Medium | Web parts with SharePoint CSOM/REST API calls, basic business logic | 3-5 days per web part |
| Complex | Web parts with server-side object model, SQL connections, complex logic | 1-2 weeks per web part |
| Very complex | Full-trust code, custom service applications, GAC assemblies | 2-4 weeks; may require Azure services |
| Not portable | Deep server-side integration (ULS logging, health analyzer, admin pages) | Redesign from scratch with Azure services |
3. SPFx development fundamentals¶
SPFx project setup¶
# Install Node.js LTS (18.x or later)
# Install Yeoman and SPFx generator
npm install -g yo @microsoft/generator-sharepoint
# Create a new SPFx web part project
yo @microsoft/sharepoint
# Prompts:
# Solution name: my-custom-webpart
# Target: SharePoint Online only
# Type: WebPart
# Framework: React
# Build and test locally
gulp serve
SPFx web part structure¶
// src/webparts/myWebPart/MyWebPart.ts
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
export default class MyWebPart extends BaseClientSideWebPart<IMyWebPartProps> {
public render(): void {
// Render the React component or raw HTML
const element = React.createElement(MyComponent, {
context: this.context,
listTitle: this.properties.listTitle,
});
ReactDom.render(element, this.domElement);
}
// Replace server-side object model calls with REST API
private async getListItems(): Promise<any[]> {
const response: SPHttpClientResponse =
await this.context.spHttpClient.get(
`${this.context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${this.properties.listTitle}')/items`,
SPHttpClient.configurations.v1,
);
const data = await response.json();
return data.value;
}
}
Server-side to client-side API mapping¶
| Server-side (on-prem) | Client-side (SPFx) | Notes |
|---|---|---|
SPContext.Current.Web | this.context.pageContext.web | Web context from SPFx |
SPList list = web.Lists["Title"] | REST: /_api/web/lists/getbytitle('Title') | SPHttpClient or PnPjs |
list.Items | REST: /_api/web/lists/getbytitle('Title')/items | SPHttpClient or PnPjs |
list.Items.Add() | REST POST to /_api/web/lists/getbytitle('Title')/items | Include X-RequestDigest header |
item.Update() | REST MERGE to item endpoint | Include If-Match header for concurrency |
SPSecurity.RunWithElevatedPrivileges | App-only auth (Azure AD app registration) | Different security model |
web.CurrentUser | this.context.pageContext.user | Current user context |
SPUtility.SendEmail | Microsoft Graph API: /me/sendMail | Requires Graph API permissions |
SPTimerJob | Azure Functions (timer trigger) | See section 5 |
SPEventReceiver | SPO webhooks + Azure Function | See section 6 |
SPUserProfileManager | Microsoft Graph API: /users/{id} | User profiles via Graph |
4. SharePoint Designer customization migration¶
Custom pages and views¶
SharePoint Designer custom pages (aspx pages with custom markup) do not work in modern SPO. Migration options:
- Modern page equivalent: Recreate the page using modern web parts and page sections
- SPFx full-page application: For complex custom pages, create an SPFx extension hosted in a full-page experience
- Power Apps: For data-driven pages, embed a Power Apps canvas app in a modern page
Custom XSLT list views¶
XSLT Data View Web Parts (DVWPs) created in SharePoint Designer are not supported in modern SPO.
| DVWP scenario | Modern equivalent |
|---|---|
| Custom list rendering | Modern list with column formatting (JSON) |
| Aggregated views (cross-list) | Highlighted content web part or SPFx web part |
| Custom grouping/sorting | Modern list views with grouping |
| Conditional formatting | Column formatting with JSON |
| Custom forms | Power Apps list form customization |
Column formatting with JSON (replacing XSLT)¶
{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "div",
"style": {
"background-color": "=if(@currentField == 'Critical', '#FDE7E9', if(@currentField == 'High', '#FFF4CE', '#DFF6DD'))"
},
"children": [
{
"elmType": "span",
"txtContent": "@currentField",
"style": {
"font-weight": "600"
}
}
]
}
5. Timer job replacement with Azure Functions¶
On-premises timer job pattern¶
// On-premises: Custom timer job
public class DailyReportJob : SPJobDefinition
{
public override void Execute(Guid targetInstanceId)
{
using (SPSite site = new SPSite("https://sharepoint.contoso.com/sites/reports"))
{
SPWeb web = site.RootWeb;
SPList list = web.Lists["Daily Reports"];
// Process items, generate reports, send emails
}
}
}
Azure Functions replacement¶
// Azure Function: Timer-triggered (equivalent)
[FunctionName("DailyReportJob")]
public static async Task Run(
[TimerTrigger("0 0 2 * * *")] TimerInfo timer, // Daily at 2:00 AM
ILogger log)
{
var credential = new DefaultAzureCredential();
var graphClient = new GraphServiceClient(credential);
// Use Microsoft Graph API instead of server-side object model
var items = await graphClient.Sites["{site-id}"]
.Lists["{list-id}"]
.Items
.Request()
.GetAsync();
foreach (var item in items)
{
// Process items
log.LogInformation($"Processing: {item.Fields.AdditionalData["Title"]}");
}
// Send email via Graph API
await graphClient.Users["{user-id}"]
.SendMail(message, false)
.Request()
.PostAsync();
}
6. Event receiver replacement with webhooks¶
On-premises event receiver¶
// On-premises: Item event receiver
public class DocumentEventReceiver : SPItemEventReceiver
{
public override void ItemAdded(SPItemEventProperties properties)
{
SPListItem item = properties.ListItem;
// Custom logic when document is added
item["ReviewStatus"] = "Pending";
item.Update();
}
}
SPO webhook + Azure Function replacement¶
// Step 1: Register webhook on SPO list
// POST https://contoso.sharepoint.com/sites/docs/_api/web/lists('{list-id}')/subscriptions
// Body: { "resource": "list-url", "notificationUrl": "https://my-function.azurewebsites.net/api/webhook", "expirationDateTime": "2027-01-01" }
// Step 2: Azure Function to handle webhook
[FunctionName("SharePointWebhook")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
ILogger log)
{
// Handle validation handshake
string validationToken = req.Query["validationtoken"];
if (!string.IsNullOrEmpty(validationToken))
return new OkObjectResult(validationToken);
// Process notification
string body = await new StreamReader(req.Body).ReadToEndAsync();
var notification = JsonConvert.DeserializeObject<WebhookNotification>(body);
// Get changed items using GetChanges API
// Process each change
return new OkResult();
}
7. Master page and branding migration¶
Modern theming (replacing master pages)¶
# Create a custom theme using PnP PowerShell
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive
$theme = @{
"themePrimary" = "#0078d4"
"themeLighterAlt" = "#f3f9fd"
"themeLighter" = "#d0e7f8"
"themeLight" = "#a9d3f2"
"themeTertiary" = "#5ba9e5"
"themeSecondary" = "#1a86d9"
"themeDarkAlt" = "#006cbe"
"themeDark" = "#005ba1"
"themeDarker" = "#004377"
"neutralLighterAlt" = "#faf9f8"
"neutralLighter" = "#f3f2f1"
"neutralLight" = "#edebe9"
"neutralQuaternaryAlt" = "#e1dfdd"
"neutralQuaternary" = "#d0d0d0"
"neutralTertiaryAlt" = "#c8c6c4"
"neutralTertiary" = "#a19f9d"
"neutralSecondary" = "#605e5c"
"neutralPrimaryAlt" = "#3b3a39"
"neutralPrimary" = "#323130"
"neutralDark" = "#201f1e"
"black" = "#000000"
"white" = "#ffffff"
}
Add-PnPTenantTheme -Identity "Contoso Corporate" -Palette $theme -IsInverted $false
SPFx application customizer (header/footer)¶
For branding elements that require more than JSON themes (custom header, footer, notification bar):
// SPFx Application Customizer for custom header/footer
import {
BaseApplicationCustomizer,
PlaceholderName,
} from "@microsoft/sp-application-base";
export default class HeaderFooterCustomizer extends BaseApplicationCustomizer<IHeaderFooterProps> {
public onInit(): Promise<void> {
// Render custom header
const headerPlaceholder =
this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
);
if (headerPlaceholder) {
headerPlaceholder.domElement.innerHTML = `
<div class="custom-header">
<img src="/sites/branding/logo.png" alt="Contoso" />
<span>Contoso Intranet</span>
</div>
`;
}
// Render custom footer
const footerPlaceholder =
this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Bottom,
);
if (footerPlaceholder) {
footerPlaceholder.domElement.innerHTML = `
<div class="custom-footer">
<span>© 2026 Contoso. All rights reserved.</span>
</div>
`;
}
return Promise.resolve();
}
}
8. Deployment and governance¶
SPFx solution deployment¶
# Build and package SPFx solution
gulp bundle --ship
gulp package-solution --ship
# Deploy to tenant app catalog
Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/appcatalog" -Interactive
Add-PnPApp -Path ".\sharepoint\solution\my-webpart.sppkg" `
-Scope Tenant `
-Publish `
-Overwrite
# Deploy to a specific site
Install-PnPApp -Identity "my-webpart-client-side-solution" -Scope Site
Governance for custom solutions in SPO¶
| Control | Implementation |
|---|---|
| App catalog access | Restrict who can upload to tenant app catalog |
| Site collection app catalogs | Enable per-site catalogs for isolated deployments |
| API permissions | Review and approve Graph API permissions in SharePoint admin center |
| ALM (Application Lifecycle Management) | Use Azure DevOps or GitHub Actions for CI/CD |
| Testing | SPFx unit testing with Jest; integration testing with Playwright |
| Monitoring | Application Insights integration for SPFx telemetry |
9. Solutions with no SPFx equivalent¶
Some farm solution capabilities have no client-side equivalent and require cloud services:
| Capability | Cloud alternative | Notes |
|---|---|---|
| Custom search crawl connectors | Microsoft Graph connectors | Index external content in Microsoft Search |
| Custom authentication providers | Entra ID (Azure AD) | SAML, OIDC, custom providers via Entra |
| Server-side file processing | Azure Functions + Azure Blob | Process files uploaded to SPO via webhooks |
| Custom logging (ULS) | Application Insights | Centralized telemetry for SPFx and Azure Functions |
| Health monitoring rules | Azure Monitor alerts | Custom health checks via Azure Monitor |
| Custom Central Admin pages | Microsoft 365 admin center + custom admin app | No extensibility in M365 admin center; build standalone |
References¶
- SharePoint Framework documentation
- SPFx web part development
- SPFx extensions
- SharePoint webhooks
- Azure Functions documentation
- Microsoft Graph API
- PnP SPFx controls
- Modern theming
Maintainers: csa-inabox core team Last updated: 2026-04-30