Cesivi Server - Extensibility Guide¶
Version: 1.0 Date: 2026-01-11 Target Audience: Developers building custom extensions for Cesivi Server
Quick Links¶
📖 API Reference Documentation - Complete technical API reference for all extensibility interfaces, classes, and types.
Table of Contents¶
- Overview
- Architecture
- Extension Points Reference
- Custom Field Renderers
- Custom WebParts
- Custom Extensions
- Custom Layouts
- Plugin Development
- Example Projects
- Best Practices
- API Reference
Overview¶
Cesivi Server (SPM) provides a comprehensive extensibility model inspired by SharePoint Classic On-Premises, allowing developers to create custom extensions, field renderers, WebParts, and layouts using modern .NET technologies.
Why Extensibility?¶
"I want to have an extensible UI like SharePoint OnPremises CLASSIC EXPERIENCE had it. It makes SPM a real platform for later. A base technology for customer projects." — Project Vision
Key Features¶
- Extension Points - Inject custom content at predefined locations
- Field Renderers - Custom display and edit controls for field types
- WebParts - Pluggable components for page composition
- Custom Layouts - Master page customization
- Plugin System - NuGet package deployment
Technology Stack¶
- ASP.NET Core 9.0 - Web framework
- Razor Pages - Server-side rendering
- Blazor Server - Interactive components (file upload, real-time UI)
- Dependency Injection - Component registration and lifecycle
- CSS Variables - Theme system
Architecture¶
Extensibility Model¶
SPM's extensibility model follows a layered architecture:
┌─────────────────────────────────────────────────┐
│ Customer Extensions (NuGet Packages) │
│ - Custom Field Renderers │
│ - Custom WebParts │
│ - Custom Extensions (Footer, Analytics, etc.) │
├─────────────────────────────────────────────────┤
│ Extensibility Layer │
│ - ICesiviExtension + ExtensionManager │
│ - IFieldRenderer + FieldRendererManager │
│ - ICesiviWebPart + WebPartManager │
├─────────────────────────────────────────────────┤
│ Built-in Components │
│ - 10 Field Renderers (Text, Choice, User, etc.) │
│ - 4 WebParts (ListView, ContentEditor, etc.) │
│ - Layout System (TopBar, QuickLaunch, Ribbon) │
├─────────────────────────────────────────────────┤
│ Core Infrastructure │
│ - REST/SOAP/CSOM APIs │
│ - Storage Service (SQL/PostgreSQL/LiteDb) │
│ - Distributed State (Redis/InMemory) │
└─────────────────────────────────────────────────┘
Core Interfaces¶
| Interface | Purpose | Example Use Cases |
|---|---|---|
ICesiviExtension |
Inject HTML at extension points | Custom footer, analytics script, banner |
IFieldRenderer |
Custom field display/edit | Star rating, color picker, WYSIWYG editor |
ICesiviWebPart |
Pluggable page components | Weather widget, news feed, charts |
Dependency Injection¶
All extensibility components are registered via DI in Program.cs:
// Register extension
services.AddSingleton<ICesiviExtension, MyCustomExtension>();
// Register field renderer
services.AddSingleton<IFieldRenderer, MyFieldRenderer>();
// Register webpart
services.AddSingleton<ICesiviWebPart, MyWebPart>();
Extension Points Reference¶
Extension points are predefined locations in the UI where custom extensions can inject content.
Layout Extension Points¶
Defined in _Layout.cshtml:
| Extension Point | Location | Common Uses |
|---|---|---|
Head |
Inside <head> tag |
Meta tags, custom CSS |
Styles |
After theme CSS | Additional stylesheets |
BeforeContent |
Before main content area | Announcements, breadcrumbs |
AfterContent |
After main content area | Footer, disclaimers |
Scripts |
End of <body> |
Custom JavaScript |
Example Usage:
@inject ExtensionManager ExtensionManager
<!DOCTYPE html>
<html>
<head>
@await ExtensionManager.RenderExtensionPointAsync("Head", new ExtensionContext
{
HttpContext = this.Context,
ExtensionPointName = "Head",
CurrentWeb = Model.CurrentWeb
})
<link rel="stylesheet" href="/css/spm-theme.css" />
@await ExtensionManager.RenderExtensionPointAsync("Styles", context)
</head>
<body>
<div class="main-content">
@await ExtensionManager.RenderExtensionPointAsync("BeforeContent", context)
@RenderBody()
@await ExtensionManager.RenderExtensionPointAsync("AfterContent", context)
</div>
@await ExtensionManager.RenderExtensionPointAsync("Scripts", context)
</body>
</html>
Component Extension Points¶
TopBar Extension Points¶
| Extension Point | Location | Common Uses |
|---|---|---|
TopBarLeft |
After brand logo | Custom navigation links |
TopBarRight |
Before theme switcher | User profile, notifications |
TopBarActions |
After Settings/Help | Custom action buttons |
QuickLaunch Extension Points¶
| Extension Point | Location | Common Uses |
|---|---|---|
QuickLaunchTop |
Above standard menu | Shortcuts, favorites |
QuickLaunchBottom |
Below standard menu | External links, tools |
Ribbon Extension Points¶
| Extension Point | Location | Common Uses |
|---|---|---|
RibbonTabs |
Custom ribbon tabs | Context-specific actions |
RibbonCommands |
Custom command groups | Bulk operations, exports |
Breadcrumb Extension Points¶
| Extension Point | Location | Common Uses |
|---|---|---|
BreadcrumbBefore |
Before breadcrumb trail | Back button, home icon |
BreadcrumbActions |
After breadcrumb trail | Page actions, share button |
Form Extension Points¶
| Extension Point | Location | Common Uses |
|---|---|---|
FormHeader |
Above form fields | Instructions, warnings |
FormFooter |
Below form fields | Disclaimers, help links |
OnValidating |
Before field validation | Custom validation logic |
OnSaving |
Before item save | Business rules, logging |
OnSaved |
After item save | Notifications, workflows |
List View Extension Points¶
| Extension Point | Location | Common Uses |
|---|---|---|
ListViewAboveGrid |
Above list grid | Filters, search box |
ListViewToolbar |
List toolbar | Custom actions, exports |
ListViewBelowGrid |
Below list grid | Summary, charts |
Custom Field Renderers¶
Field renderers control how field values are displayed and edited in forms.
IFieldRenderer Interface¶
namespace Cesivi.WebUI.FieldRenderers
{
public interface IFieldRenderer
{
/// <summary>
/// Field type this renderer handles (e.g., "Text", "Choice", "MyCustomType")
/// </summary>
string FieldType { get; }
/// <summary>
/// Render field value in display mode (DispForm.cshtml)
/// </summary>
Task<IHtmlContent> RenderDisplayAsync(SpField field, object value, FieldRenderContext context);
/// <summary>
/// Render field editor in edit mode (EditForm.cshtml, NewForm.cshtml)
/// </summary>
Task<IHtmlContent> RenderEditAsync(SpField field, object value, FieldRenderContext context);
/// <summary>
/// Parse form submission back to field value
/// </summary>
Task<object> ParseValueAsync(SpField field, IFormCollection form, FieldRenderContext context);
}
}
FieldRenderContext¶
public class FieldRenderContext
{
/// <summary>HTTP request context</summary>
public HttpContext HttpContext { get; set; }
/// <summary>Current web</summary>
public SpWeb CurrentWeb { get; set; }
/// <summary>Current list (if any)</summary>
public SpList? CurrentList { get; set; }
/// <summary>Current item (if any)</summary>
public SpListItem? CurrentItem { get; set; }
/// <summary>True if in edit/new mode, false if display mode</summary>
public bool IsEditMode { get; set; }
/// <summary>Additional parameters</summary>
public Dictionary<string, object> Parameters { get; set; } = new();
}
Built-in Field Renderers¶
SPM includes 10 built-in renderers:
| Renderer | Field Type | Display | Edit |
|---|---|---|---|
TextFieldRenderer |
Text, Note | Plain text / Rich HTML | Input / Textarea |
NumberFieldRenderer |
Number, Integer | Formatted number | Number input |
BooleanFieldRenderer |
Boolean | Yes/No | Checkbox |
DateTimeFieldRenderer |
DateTime | Formatted date/time | Date picker |
ChoiceFieldRenderer |
Choice, MultiChoice | Selected values | Dropdown / Checkboxes |
CurrencyFieldRenderer |
Currency | $1,234.56 | Currency input |
UrlFieldRenderer |
URL | Hyperlink | URL + description inputs |
LookupFieldRenderer |
Lookup | Linked item title | Lookup dropdown |
UserFieldRenderer |
User | User display name | User picker |
AttachmentFieldRenderer |
Attachments | File links | File upload |
Example: Star Rating Field Renderer¶
This example creates a custom star rating field renderer (1-5 stars).
Step 1: Create the Renderer
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Cesivi.WebUI.FieldRenderers;
using Cesivi.WebUI.Models;
namespace MyCompany.CesiviExtensions.FieldRenderers
{
public class StarRatingFieldRenderer : IFieldRenderer
{
public string FieldType => "StarRating";
public Task<IHtmlContent> RenderDisplayAsync(SpField field, object value, FieldRenderContext context)
{
var rating = Convert.ToInt32(value ?? 0);
var stars = string.Join("", Enumerable.Repeat("⭐", rating));
var emptyStars = string.Join("", Enumerable.Repeat("☆", 5 - rating));
var html = $@"
<span class='star-rating' title='{rating} out of 5 stars'>
<span class='stars-filled'>{stars}</span>
<span class='stars-empty'>{emptyStars}</span>
<span class='rating-number'>({rating}/5)</span>
</span>";
return Task.FromResult<IHtmlContent>(new HtmlString(html));
}
public Task<IHtmlContent> RenderEditAsync(SpField field, object value, FieldRenderContext context)
{
var rating = Convert.ToInt32(value ?? 0);
var fieldName = field.InternalName;
var html = $@"
<div class='star-rating-editor'>
<select name='{fieldName}' id='{fieldName}' class='form-control'
onchange='updateStarRating(this)'>
<option value='0' {(rating == 0 ? "selected" : "")}>No rating</option>
<option value='1' {(rating == 1 ? "selected" : "")}>⭐ (1 star)</option>
<option value='2' {(rating == 2 ? "selected" : "")}>⭐⭐ (2 stars)</option>
<option value='3' {(rating == 3 ? "selected" : "")}>⭐⭐⭐ (3 stars)</option>
<option value='4' {(rating == 4 ? "selected" : "")}>⭐⭐⭐⭐ (4 stars)</option>
<option value='5' {(rating == 5 ? "selected" : "")}>⭐⭐⭐⭐⭐ (5 stars)</option>
</select>
<div class='star-rating-preview' id='{fieldName}_preview'>
{string.Join("", Enumerable.Repeat("⭐", rating))}
</div>
</div>
<script>
function updateStarRating(select) {{
var rating = parseInt(select.value);
var preview = document.getElementById(select.id + '_preview');
preview.innerHTML = '⭐'.repeat(rating);
}}
</script>";
return Task.FromResult<IHtmlContent>(new HtmlString(html));
}
public Task<object> ParseValueAsync(SpField field, IFormCollection form, FieldRenderContext context)
{
var value = form[field.InternalName].ToString();
return Task.FromResult<object>(Convert.ToInt32(value));
}
}
}
Step 2: Register the Renderer
In Program.cs:
using MyCompany.CesiviExtensions.FieldRenderers;
var builder = WebApplication.CreateBuilder(args);
// Register custom field renderer
builder.Services.AddSingleton<IFieldRenderer, StarRatingFieldRenderer>();
var app = builder.Build();
Step 3: Use the Renderer
Create a custom field type in your list:
# PowerShell example (via CSOM or PnP)
Add-PnPField -List "ProductReviews" -DisplayName "Rating" -InternalName "Rating" -Type "StarRating"
Now the field will render with stars in display mode and a dropdown in edit mode.
Example: Color Picker Field Renderer¶
public class ColorPickerFieldRenderer : IFieldRenderer
{
public string FieldType => "Color";
public Task<IHtmlContent> RenderDisplayAsync(SpField field, object value, FieldRenderContext context)
{
var color = value?.ToString() ?? "#000000";
var html = $@"
<div class='color-display'>
<span class='color-swatch' style='background-color: {color};
width: 30px;
height: 30px;
display: inline-block;
border: 1px solid #ccc;'></span>
<span class='color-value'>{color}</span>
</div>";
return Task.FromResult<IHtmlContent>(new HtmlString(html));
}
public Task<IHtmlContent> RenderEditAsync(SpField field, object value, FieldRenderContext context)
{
var color = value?.ToString() ?? "#000000";
var fieldName = field.InternalName;
var html = $@"
<input type='color'
name='{fieldName}'
id='{fieldName}'
value='{color}'
class='form-control color-picker' />";
return Task.FromResult<IHtmlContent>(new HtmlString(html));
}
public Task<object> ParseValueAsync(SpField field, IFormCollection form, FieldRenderContext context)
{
return Task.FromResult<object>(form[field.InternalName].ToString());
}
}
Custom WebParts¶
WebParts are pluggable components that can be added to pages.
ICesiviWebPart Interface¶
namespace Cesivi.WebUI.WebParts
{
public interface ICesiviWebPart
{
/// <summary>WebPart display title</summary>
string Title { get; }
/// <summary>WebPart description</summary>
string Description { get; }
/// <summary>Unique WebPart ID</summary>
Guid Id { get; }
/// <summary>WebPart category</summary>
WebPartCategory Category { get; }
/// <summary>Render WebPart HTML</summary>
Task<IHtmlContent> RenderAsync(WebPartContext context);
/// <summary>Render property editor</summary>
Task<IHtmlContent> RenderEditPropertiesAsync(WebPartContext context);
/// <summary>Save property values</summary>
Task SavePropertiesAsync(IFormCollection form, WebPartContext context);
}
}
WebPartContext¶
public class WebPartContext
{
public HttpContext HttpContext { get; set; }
public SpWeb CurrentWeb { get; set; }
public SpList? CurrentList { get; set; }
public Dictionary<string, string> Properties { get; set; } = new();
}
WebPartCategory Enum¶
public enum WebPartCategory
{
Lists, // List and library webparts
Content, // Text, HTML content
Media, // Images, videos
Forms, // Form-based webparts
Search, // Search-related
Social, // Social features
Charts, // Charts and visualizations
Custom // Customer-specific
}
Built-in WebParts¶
| WebPart | Category | Description |
|---|---|---|
ListViewWebPart |
Lists | Display list items in a grid |
ContentEditorWebPart |
Content | HTML/Rich text editor |
ImageWebPart |
Media | Display image with optional hyperlink |
ScriptEditorWebPart |
Custom | Custom JavaScript/CSS injection |
Example: Weather WebPart¶
This example creates a weather forecast webpart.
Step 1: Create the WebPart
using System.Net.Http;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Cesivi.WebUI.WebParts;
namespace MyCompany.CesiviExtensions.WebParts
{
[SpmWebPart("MyCompany.Weather", Category = WebPartCategory.Custom)]
public class WeatherWebPart : ICesiviWebPart
{
private readonly IHttpClientFactory _httpClientFactory;
public WeatherWebPart(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public string Title => "Weather Forecast";
public string Description => "Shows weather forecast for configured location";
public Guid Id => Guid.Parse("12345678-1234-1234-1234-123456789012");
public WebPartCategory Category => WebPartCategory.Custom;
public async Task<IHtmlContent> RenderAsync(WebPartContext context)
{
var location = context.Properties.GetValueOrDefault("Location", "New York");
var apiKey = context.Properties.GetValueOrDefault("ApiKey", "");
if (string.IsNullOrEmpty(apiKey))
{
return new HtmlString(@"
<div class='alert alert-warning'>
<strong>Configuration Required:</strong>
Please configure API key in webpart properties.
</div>");
}
var weatherData = await GetWeatherAsync(location, apiKey);
var html = $@"
<div class='weather-webpart'>
<div class='weather-header'>
<h3>
<i class='fa fa-cloud'></i>
Weather in {location}
</h3>
</div>
<div class='weather-body'>
<div class='weather-temp'>
<span class='temp-value'>{weatherData.Temperature}°C</span>
</div>
<div class='weather-conditions'>
<i class='{GetWeatherIcon(weatherData.Conditions)}'></i>
<span>{weatherData.Conditions}</span>
</div>
<div class='weather-details'>
<div class='detail-item'>
<span class='label'>Humidity:</span>
<span class='value'>{weatherData.Humidity}%</span>
</div>
<div class='detail-item'>
<span class='label'>Wind:</span>
<span class='value'>{weatherData.WindSpeed} km/h</span>
</div>
</div>
</div>
</div>";
return new HtmlString(html);
}
public async Task<IHtmlContent> RenderEditPropertiesAsync(WebPartContext context)
{
var location = context.Properties.GetValueOrDefault("Location", "New York");
var apiKey = context.Properties.GetValueOrDefault("ApiKey", "");
var html = $@"
<div class='webpart-properties'>
<div class='form-group'>
<label for='Location'>Location:</label>
<input type='text'
name='Location'
id='Location'
value='{location}'
class='form-control'
placeholder='City name' />
<small class='form-text text-muted'>
Enter the city name for weather forecast
</small>
</div>
<div class='form-group'>
<label for='ApiKey'>API Key:</label>
<input type='text'
name='ApiKey'
id='ApiKey'
value='{apiKey}'
class='form-control'
placeholder='Your weather API key' />
<small class='form-text text-muted'>
Get a free API key from <a href='https://openweathermap.org/api' target='_blank'>OpenWeatherMap</a>
</small>
</div>
</div>";
return new HtmlString(html);
}
public Task SavePropertiesAsync(IFormCollection form, WebPartContext context)
{
context.Properties["Location"] = form["Location"].ToString();
context.Properties["ApiKey"] = form["ApiKey"].ToString();
return Task.CompletedTask;
}
private async Task<WeatherData> GetWeatherAsync(string location, string apiKey)
{
var client = _httpClientFactory.CreateClient();
var url = $"https://api.openweathermap.org/data/2.5/weather?q={location}&appid={apiKey}&units=metric";
try
{
var response = await client.GetStringAsync(url);
// Parse JSON response (simplified)
return new WeatherData
{
Temperature = 22,
Conditions = "Sunny",
Humidity = 65,
WindSpeed = 12
};
}
catch
{
return new WeatherData { Temperature = 0, Conditions = "Unknown" };
}
}
private string GetWeatherIcon(string conditions)
{
return conditions.ToLower() switch
{
"sunny" or "clear" => "fa fa-sun",
"cloudy" or "clouds" => "fa fa-cloud",
"rain" or "rainy" => "fa fa-cloud-rain",
"snow" => "fa fa-snowflake",
_ => "fa fa-question"
};
}
private class WeatherData
{
public int Temperature { get; set; }
public string Conditions { get; set; }
public int Humidity { get; set; }
public int WindSpeed { get; set; }
}
}
}
Step 2: Register the WebPart
using MyCompany.CesiviExtensions.WebParts;
var builder = WebApplication.CreateBuilder(args);
// Register HTTP client factory (required by WeatherWebPart)
builder.Services.AddHttpClient();
// Register custom webpart
builder.Services.AddSingleton<ICesiviWebPart, WeatherWebPart>();
var app = builder.Build();
Step 3: Add to a Page
The WebPart will appear in the WebPart gallery and can be added to any page with a WebPart zone.
Example: Chart WebPart¶
public class ChartWebPart : ICesiviWebPart
{
public string Title => "Chart Viewer";
public string Description => "Display data as charts (bar, line, pie)";
public Guid Id => Guid.Parse("CHART001-1234-1234-1234-123456789012");
public WebPartCategory Category => WebPartCategory.Charts;
public async Task<IHtmlContent> RenderAsync(WebPartContext context)
{
var chartType = context.Properties.GetValueOrDefault("ChartType", "bar");
var dataSource = context.Properties.GetValueOrDefault("DataSource", "");
// Render chart using Chart.js or similar library
var html = $@"
<div class='chart-webpart'>
<canvas id='chart_{Id}'></canvas>
</div>
<script src='https://cdn.jsdelivr.net/npm/chart.js'></script>
<script>
var ctx = document.getElementById('chart_{Id}').getContext('2d');
var chart = new Chart(ctx, {{
type: '{chartType}',
data: {{
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [{{
label: 'Data',
data: [12, 19, 3, 5, 2]
}}]
}}
}});
</script>";
return new HtmlString(html);
}
// ... (RenderEditPropertiesAsync and SavePropertiesAsync omitted for brevity)
}
Custom Extensions¶
Extensions inject custom HTML at predefined extension points.
ICesiviExtension Interface¶
namespace Cesivi.WebUI.Extensibility
{
public interface ICesiviExtension
{
/// <summary>Extension name (unique identifier)</summary>
string Name { get; }
/// <summary>Rendering priority (lower = earlier)</summary>
int Priority { get; }
/// <summary>Render extension HTML</summary>
Task<IHtmlContent> RenderAsync(ExtensionContext context);
}
}
ExtensionContext¶
public class ExtensionContext
{
public HttpContext HttpContext { get; set; }
public string ExtensionPointName { get; set; }
public SpWeb CurrentWeb { get; set; }
public SpList? CurrentList { get; set; }
public SpListItem? CurrentItem { get; set; }
public Dictionary<string, object> Parameters { get; set; } = new();
}
Example: Custom Footer Extension¶
using Microsoft.AspNetCore.Html;
using Cesivi.WebUI.Extensibility;
namespace MyCompany.CesiviExtensions.Extensions
{
public class CustomFooterExtension : ICesiviExtension
{
public string Name => "MyCompany.CustomFooter";
public int Priority => 100;
public async Task<IHtmlContent> RenderAsync(ExtensionContext context)
{
// Only render on "AfterContent" extension point
if (context.ExtensionPointName != "AfterContent")
return HtmlString.Empty;
var currentYear = DateTime.Now.Year;
var html = $@"
<footer class='custom-footer'>
<div class='footer-content'>
<div class='footer-left'>
<p>© {currentYear} MyCompany. All rights reserved.</p>
</div>
<div class='footer-center'>
<p>Powered by <strong>Cesivi Server</strong></p>
</div>
<div class='footer-right'>
<a href='/privacy'>Privacy Policy</a> |
<a href='/terms'>Terms of Service</a>
</div>
</div>
</footer>
<style>
.custom-footer {{
background-color: var(--spm-surface);
border-top: 1px solid var(--spm-border);
padding: var(--spm-spacing-lg);
margin-top: var(--spm-spacing-xl);
}}
.custom-footer .footer-content {{
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}}
.custom-footer a {{
color: var(--spm-primary);
text-decoration: none;
}}
.custom-footer a:hover {{
text-decoration: underline;
}}
</style>";
return new HtmlString(html);
}
}
}
Example: Analytics Extension¶
public class AnalyticsExtension : ICesiviExtension
{
public string Name => "MyCompany.Analytics";
public int Priority => 10; // Low priority = renders early
public async Task<IHtmlContent> RenderAsync(ExtensionContext context)
{
// Only render on "Scripts" extension point
if (context.ExtensionPointName != "Scripts")
return HtmlString.Empty;
var trackingId = "UA-123456-1"; // Replace with your GA tracking ID
var html = $@"
<!-- Google Analytics -->
<script async src='https://www.googletagmanager.com/gtag/js?id={trackingId}'></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){{dataLayer.push(arguments);}}
gtag('js', new Date());
gtag('config', '{trackingId}');
</script>";
return new HtmlString(html);
}
}
Example: Announcement Banner Extension¶
public class AnnouncementBannerExtension : ICesiviExtension
{
public string Name => "MyCompany.AnnouncementBanner";
public int Priority => 50;
public async Task<IHtmlContent> RenderAsync(ExtensionContext context)
{
// Only render on "BeforeContent" extension point
if (context.ExtensionPointName != "BeforeContent")
return HtmlString.Empty;
// Check if user has dismissed the banner (cookie/localStorage)
var html = @"
<div class='announcement-banner' id='announcementBanner'>
<div class='banner-content'>
<i class='fa fa-bullhorn'></i>
<span>System maintenance scheduled for Saturday 2 AM - 6 AM EST.</span>
<button onclick='dismissBanner()' class='btn-close'>×</button>
</div>
</div>
<script>
function dismissBanner() {
document.getElementById('announcementBanner').style.display = 'none';
localStorage.setItem('banner_dismissed', 'true');
}
if (localStorage.getItem('banner_dismissed') === 'true') {
document.getElementById('announcementBanner').style.display = 'none';
}
</script>
<style>
.announcement-banner {
background-color: #fff3cd;
border-bottom: 1px solid #ffecb5;
padding: 12px 16px;
text-align: center;
}
.announcement-banner .banner-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.announcement-banner .btn-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
</style>";
return new HtmlString(html);
}
}
Custom Layouts¶
Custom layouts allow you to modify the master page structure.
Layout System¶
SPM uses Razor Layouts for master page functionality:
_Layout.cshtml- Main layout_LoginLayout.cshtml- Login page layout- Custom layouts can be created per web or site collection
Creating a Custom Layout¶
Step 1: Create Custom Layout File
Views/Shared/_CustomLayout.cshtml:
@inject ExtensionManager ExtensionManager
@{
var context = new ExtensionContext
{
HttpContext = this.Context,
CurrentWeb = ViewBag.CurrentWeb
};
}
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Cesivi Server</title>
<!-- Theme CSS -->
<link rel="stylesheet" href="/css/spm-theme.css" />
<link rel="stylesheet" href="/css/custom-theme.css" />
<!-- Extension Point: Head -->
@{
context.ExtensionPointName = "Head";
}
@await ExtensionManager.RenderExtensionPointAsync("Head", context)
<!-- Extension Point: Styles -->
@{
context.ExtensionPointName = "Styles";
}
@await ExtensionManager.RenderExtensionPointAsync("Styles", context)
</head>
<body>
<!-- Custom Header -->
<header class="custom-header">
<div class="container">
<div class="logo">
<img src="/images/company-logo.png" alt="Company Logo" />
</div>
@await Component.InvokeAsync("TopBar")
</div>
</header>
<!-- Main Content Area -->
<div class="main-container">
<aside class="sidebar">
@await Component.InvokeAsync("QuickLaunch")
</aside>
<main class="content-area">
<!-- Extension Point: BeforeContent -->
@{
context.ExtensionPointName = "BeforeContent";
}
@await ExtensionManager.RenderExtensionPointAsync("BeforeContent", context)
<!-- Page Content -->
@RenderBody()
<!-- Extension Point: AfterContent -->
@{
context.ExtensionPointName = "AfterContent";
}
@await ExtensionManager.RenderExtensionPointAsync("AfterContent", context)
</main>
</div>
<!-- Extension Point: Scripts -->
@{
context.ExtensionPointName = "Scripts";
}
@await ExtensionManager.RenderExtensionPointAsync("Scripts", context)
</body>
</html>
Step 2: Use Custom Layout
In a Razor Page:
@page
@model MyPageModel
@{
Layout = "_CustomLayout";
ViewData["Title"] = "My Custom Page";
}
<h1>Welcome to My Custom Page</h1>
Layout Best Practices¶
- Always include extension points - Maintain extensibility
- Use CSS variables - Respect theme system
- Keep responsive - Support mobile devices
- Preserve accessibility - ARIA labels, semantic HTML
- Test across themes - Light and dark themes
Plugin Development¶
Plugins are NuGet packages containing custom extensions.
Plugin Structure¶
MyCompany.CesiviExtensions/
├── FieldRenderers/
│ ├── StarRatingFieldRenderer.cs
│ └── ColorPickerFieldRenderer.cs
├── WebParts/
│ ├── WeatherWebPart.cs
│ └── ChartWebPart.cs
├── Extensions/
│ ├── CustomFooterExtension.cs
│ └── AnalyticsExtension.cs
├── wwwroot/
│ ├── css/
│ │ └── extensions.css
│ └── js/
│ └── extensions.js
├── MyCompany.CesiviExtensions.csproj
└── README.md
Creating a Plugin Project¶
Step 1: Create Class Library
dotnet new classlib -n MyCompany.CesiviExtensions
cd MyCompany.CesiviExtensions
Step 2: Add Package References
MyCompany.CesiviExtensions.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>1.0.0</Version>
<Authors>MyCompany</Authors>
<Description>Custom extensions for Cesivi Server</Description>
<PackageTags>SharePoint;SPM;Extensions</PackageTags>
</PropertyGroup>
<ItemGroup>
<!-- Reference SPM WebUI interfaces -->
<PackageReference Include="Cesivi.WebUI" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Include static files in NuGet package -->
<Content Include="wwwroot\**\*">
<Pack>true</Pack>
<PackagePath>content\wwwroot\</PackagePath>
</Content>
</ItemGroup>
</Project>
Step 3: Implement Extensions
(See examples above)
Step 4: Build Package
dotnet pack
This creates bin/Release/MyCompany.CesiviExtensions.1.0.0.nupkg.
Installing a Plugin¶
Option 1: Local NuGet Package
# In SPM project folder
dotnet add Cesivi.WebUI package MyCompany.CesiviExtensions --source /path/to/packages
Option 2: NuGet Gallery
Publish to NuGet.org or private feed, then:
dotnet add Cesivi.WebUI package MyCompany.CesiviExtensions
Option 3: Project Reference (Development)
<!-- In Cesivi.WebUI.csproj -->
<ItemGroup>
<ProjectReference Include="..\MyCompany.CesiviExtensions\MyCompany.CesiviExtensions.csproj" />
</ItemGroup>
Registering Plugin Components¶
In Program.cs:
using MyCompany.CesiviExtensions.FieldRenderers;
using MyCompany.CesiviExtensions.WebParts;
using MyCompany.CesiviExtensions.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Auto-register all IFieldRenderer implementations
builder.Services.Scan(scan => scan
.FromAssembliesOf(typeof(StarRatingFieldRenderer))
.AddClasses(classes => classes.AssignableTo<IFieldRenderer>())
.As<IFieldRenderer>()
.WithSingletonLifetime());
// Auto-register all ICesiviWebPart implementations
builder.Services.Scan(scan => scan
.FromAssembliesOf(typeof(WeatherWebPart))
.AddClasses(classes => classes.AssignableTo<ICesiviWebPart>())
.As<ICesiviWebPart>()
.WithSingletonLifetime());
// Auto-register all ICesiviExtension implementations
builder.Services.Scan(scan => scan
.FromAssembliesOf(typeof(CustomFooterExtension))
.AddClasses(classes => classes.AssignableTo<ICesiviExtension>())
.As<ICesiviExtension>()
.WithSingletonLifetime());
var app = builder.Build();
Note: Requires
Scrutorpackage for assembly scanning:dotnet add package Scrutor
Example Projects¶
Example 1: Simple Field Renderer Plugin¶
Project: MyCompany.FieldRenderers
Contains 3 custom field renderers: - Star Rating (1-5 stars) - Color Picker (hex color selector) - Emoji Picker (emoji dropdown)
GitHub: https://github.com/mycompany/spm-field-renderers-example
Example 2: Dashboard WebParts Plugin¶
Project: MyCompany.DashboardWebParts
Contains 4 dashboard webparts: - Chart WebPart (Bar, Line, Pie charts) - KPI WebPart (Key performance indicators) - News Feed WebPart (RSS/Atom feed reader) - Task Summary WebPart (Task list summary)
GitHub: https://github.com/mycompany/spm-dashboard-webparts
Example 3: Corporate Branding Plugin¶
Project: MyCompany.CorporateBranding
Complete branding package: - Custom footer extension - Custom header extension - Analytics extension - Corporate theme (CSS variables) - Custom layouts
GitHub: https://github.com/mycompany/spm-corporate-branding
Example 4: Authentication Extension¶
Project: CesiviExtensions.Authentication
Location: examples/CesiviExtensions.Authentication/
Enterprise authentication integration examples: - SAML 2.0 Authentication Extension (SSO with Identity Providers) - OAuth 2.0 Authentication Extension (GitHub, Azure AD, custom providers) - Claim mapping and user profile synchronization - Login button UI integration
README: See examples/CesiviExtensions.Authentication/README.md
Example 5: Workflow Activities¶
Project: CesiviExtensions.Workflows
Location: examples/CesiviExtensions.Workflows/
Custom workflow activity examples for Kenaflow integration: - Send Email Workflow Activity (SMTP with variable substitution) - Call External API Workflow Activity (REST integration with authentication) - Custom Approval Workflow Activity (Multi-stage approvals with delegation) - Workflow variable substitution and context management
README: See examples/CesiviExtensions.Workflows/README.md
Example 6: Custom Field Types¶
Project: CesiviExtensions.CustomFields
Location: examples/CesiviExtensions.CustomFields/
Complete custom field type implementation: - Signature Field (HTML5 Canvas-based signature capture) - Storage, validation, and rendering - Cryptographic verification and tamper detection - Mobile-friendly touch support - Thumbnail generation for list views
README: See examples/CesiviExtensions.CustomFields/README.md
Example 7: Custom REST API Endpoints¶
Project: CesiviExtensions.CustomAPI
Location: examples/CesiviExtensions.CustomAPI/
Custom API controller examples extending /_api/ namespace:
- Custom Reporting API (list usage, user activity, CSV/Excel export)
- Custom Integration API (webhooks, data sync, external system integration)
- Health monitoring and status tracking
- Authentication and authorization integration
README: See examples/CesiviExtensions.CustomAPI/README.md
Best Practices¶
Performance¶
- Minimize HTML size - Keep rendered HTML concise
- Cache expensive operations - Use IDistributedCache for API calls
- Async all the way - Use async/await for I/O operations
- Lazy load resources - Load JavaScript/CSS only when needed
- Optimize images - Compress and use appropriate formats
Security¶
- Sanitize HTML output - Use
HtmlEncoder.Default.Encode() - Validate user input - Check all form submissions
- Respect permissions - Check user access before rendering
- Use HTTPS - Always use secure connections
- Avoid XSS - Never trust user-provided HTML
Example: Safe HTML Rendering
using System.Text.Encodings.Web;
public Task<IHtmlContent> RenderDisplayAsync(SpField field, object value, FieldRenderContext context)
{
var safeValue = HtmlEncoder.Default.Encode(value?.ToString() ?? "");
var html = $"<div class='field-value'>{safeValue}</div>";
return Task.FromResult<IHtmlContent>(new HtmlString(html));
}
Maintainability¶
- Version your APIs - Use semantic versioning (1.0.0, 1.1.0, 2.0.0)
- Document breaking changes - Maintain CHANGELOG.md
- Provide migration guides - Help users upgrade
- Write unit tests - Test renderers and webparts
- Follow naming conventions - Use consistent naming
Compatibility¶
- Target .NET 10.0 - Match Cesivi framework version
- Test across browsers - Chrome, Firefox, Safari, Edge
- Support light and dark themes - Use CSS variables
- Responsive design - Support mobile devices
- Accessibility - WCAG 2.1 Level AA compliance
Documentation¶
- README.md - Installation and usage instructions
- API documentation - XML comments on public APIs
- Examples - Working code samples
- Screenshots - Visual examples of rendered output
- Changelog - Version history and changes
Troubleshooting¶
Extension Not Rendering¶
Problem: Extension registered but not appearing in UI
Solutions:
1. Verify DI registration in Program.cs
2. Check extension point name matches exactly
3. Ensure Priority is set correctly
4. Verify extension is not filtering by context incorrectly
Debug:
public Task<IHtmlContent> RenderAsync(ExtensionContext context)
{
// Add logging
Console.WriteLine($"Extension {Name} rendering at {context.ExtensionPointName}");
// Your rendering logic
}
Field Renderer Not Found¶
Problem: Field shows default renderer instead of custom renderer
Solutions:
1. Verify FieldType property matches field type exactly
2. Check renderer is registered in DI
3. Ensure field type is spelled correctly (case-sensitive)
Debug:
// In FieldRendererManager
public IFieldRenderer GetRenderer(string fieldType)
{
var renderer = _renderers.FirstOrDefault(r => r.FieldType == fieldType);
if (renderer == null)
{
Console.WriteLine($"No renderer found for field type: {fieldType}");
Console.WriteLine($"Available renderers: {string.Join(", ", _renderers.Select(r => r.FieldType))}");
}
return renderer ?? _defaultRenderer;
}
WebPart Properties Not Saving¶
Problem: WebPart property changes not persisting
Solutions:
1. Verify SavePropertiesAsync implementation
2. Check form field names match property names
3. Ensure WebPart context is passed correctly
Debug:
public Task SavePropertiesAsync(IFormCollection form, WebPartContext context)
{
Console.WriteLine($"Saving properties: {string.Join(", ", form.Keys)}");
foreach (var key in form.Keys)
{
Console.WriteLine($"{key} = {form[key]}");
context.Properties[key] = form[key].ToString();
}
return Task.CompletedTask;
}
Support and Resources¶
Documentation¶
- Main Docs:
_docs/folder in repository - API Reference:
_docs/API_REFERENCE.md - MASTERPLAN:
_project/masterplan/CLAUDE-MASTERPLAN.md
Community¶
- GitHub Issues: Report bugs and request features
- Discussions: Ask questions and share extensions
- Wiki: Community-contributed tutorials
Commercial Support¶
For enterprise support, contact: support@mycompany.com
Appendix A: Complete Interface Reference¶
ICesiviExtension¶
public interface ICesiviExtension
{
string Name { get; }
int Priority { get; }
Task<IHtmlContent> RenderAsync(ExtensionContext context);
}
IFieldRenderer¶
public interface IFieldRenderer
{
string FieldType { get; }
Task<IHtmlContent> RenderDisplayAsync(SpField field, object value, FieldRenderContext context);
Task<IHtmlContent> RenderEditAsync(SpField field, object value, FieldRenderContext context);
Task<object> ParseValueAsync(SpField field, IFormCollection form, FieldRenderContext context);
}
ICesiviWebPart¶
public interface ICesiviWebPart
{
string Title { get; }
string Description { get; }
Guid Id { get; }
WebPartCategory Category { get; }
Task<IHtmlContent> RenderAsync(WebPartContext context);
Task<IHtmlContent> RenderEditPropertiesAsync(WebPartContext context);
Task SavePropertiesAsync(IFormCollection form, WebPartContext context);
}
Appendix B: CSS Variables Reference¶
Theme Variables¶
/* Brand Colors */
--spm-primary: #0078d4;
--spm-secondary: #2b88d8;
--spm-accent: #50e6ff;
--spm-success: #10893e;
--spm-warning: #ffb900;
--spm-error: #d13438;
--spm-info: #00bcf2;
/* Neutral Colors */
--spm-bg: #ffffff;
--spm-bg-secondary: #f9f9f9;
--spm-surface: #f3f2f1;
--spm-border: #edebe9;
--spm-border-hover: #c8c6c4;
--spm-text: #323130;
--spm-text-secondary: #605e5c;
--spm-text-disabled: #a19f9d;
--spm-text-inverse: #ffffff;
/* Spacing Scale */
--spm-spacing-xs: 4px;
--spm-spacing-sm: 8px;
--spm-spacing-md: 16px;
--spm-spacing-lg: 24px;
--spm-spacing-xl: 32px;
--spm-spacing-xxl: 48px;
/* Typography */
--spm-font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
--spm-font-size-xs: 11px;
--spm-font-size-sm: 12px;
--spm-font-size-md: 14px;
--spm-font-size-lg: 16px;
--spm-font-size-xl: 20px;
--spm-font-size-xxl: 28px;
--spm-font-weight-regular: 400;
--spm-font-weight-semibold: 600;
--spm-font-weight-bold: 700;
/* Component Dimensions */
--spm-ribbon-height: 48px;
--spm-topbar-height: 56px;
--spm-quicklaunch-width: 240px;
--spm-content-max-width: 1400px;
Appendix C: Extension Point Lifecycle¶
┌─────────────────────────────────────────────────┐
│ 1. Application Startup │
│ - ExtensionManager initialized │
│ - All ICesiviExtension instances loaded via DI │
└─────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2. Page Render │
│ - _Layout.cshtml renders │
│ - Extension points encountered │
└─────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3. ExtensionManager.RenderExtensionPointAsync() │
│ - Filter extensions by point name │
│ - Sort by Priority (ascending) │
└─────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 4. Call Each Extension.RenderAsync() │
│ - Pass ExtensionContext │
│ - Collect IHtmlContent results │
└─────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 5. Concatenate and Return HTML │
│ - Combine all extension outputs │
│ - Return to Razor template │
└─────────────────────────────────────────────────┘
API Reference¶
For complete technical API documentation, including all interface methods, properties, and code examples, see the API Reference Documentation.
The API reference provides:
- Detailed interface documentation for ICesiviExtension, IFieldRenderer, and ICesiviWebPart
- Complete method signatures with parameter descriptions
- Manager class documentation (ExtensionManager, FieldRendererManager, WebPartManager)
- Context class documentation (ExtensionContext, FieldRenderContext, WebPartContext)
- Built-in component reference (15 field renderers, 9 WebParts)
- Extension point reference table
- Registration examples and best practices
Quick Access: - ICesiviExtension API - IFieldRenderer API - ICesiviWebPart API - Extension Points Table - Built-in Components
End of Extensibility Guide
For questions or feedback, please open an issue on GitHub or contact the development team.
Version: 1.2 (PLAN-149 Phase 2.2) Last Updated: 2026-01-16 Author: Cesivi Server Team