Skip to content

Cesivi Server - Extensibility Guide

Version: 1.0 Date: 2026-01-11 Target Audience: Developers building custom extensions for Cesivi Server


📖 API Reference Documentation - Complete technical API reference for all extensibility interfaces, classes, and types.

Table of Contents

  1. Overview
  2. Architecture
  3. Extension Points Reference
  4. Custom Field Renderers
  5. Custom WebParts
  6. Custom Extensions
  7. Custom Layouts
  8. Plugin Development
  9. Example Projects
  10. Best Practices
  11. 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
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();
}
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

  1. Always include extension points - Maintain extensibility
  2. Use CSS variables - Respect theme system
  3. Keep responsive - Support mobile devices
  4. Preserve accessibility - ARIA labels, semantic HTML
  5. 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 Scrutor package 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

  1. Minimize HTML size - Keep rendered HTML concise
  2. Cache expensive operations - Use IDistributedCache for API calls
  3. Async all the way - Use async/await for I/O operations
  4. Lazy load resources - Load JavaScript/CSS only when needed
  5. Optimize images - Compress and use appropriate formats

Security

  1. Sanitize HTML output - Use HtmlEncoder.Default.Encode()
  2. Validate user input - Check all form submissions
  3. Respect permissions - Check user access before rendering
  4. Use HTTPS - Always use secure connections
  5. 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

  1. Version your APIs - Use semantic versioning (1.0.0, 1.1.0, 2.0.0)
  2. Document breaking changes - Maintain CHANGELOG.md
  3. Provide migration guides - Help users upgrade
  4. Write unit tests - Test renderers and webparts
  5. Follow naming conventions - Use consistent naming

Compatibility

  1. Target .NET 10.0 - Match Cesivi framework version
  2. Test across browsers - Chrome, Firefox, Safari, Edge
  3. Support light and dark themes - Use CSS variables
  4. Responsive design - Support mobile devices
  5. Accessibility - WCAG 2.1 Level AA compliance

Documentation

  1. README.md - Installation and usage instructions
  2. API documentation - XML comments on public APIs
  3. Examples - Working code samples
  4. Screenshots - Visual examples of rendered output
  5. 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