Skip to content

Serenity 10.4.0 Release Notes (2026-09-01)

These release notes detail the significant changes made in Serenity and StartSharp from version 10.3.7 to 10.4.0. For a complete list of changes, please refer to the Serenity Change Log.

Connection Key Fallbacks

Feature modules (Pro.Extensions, Meeting, WorkLog, etc.) traditionally required their own connection keys (e.g. ProMeeting, ProFeatures) to be explicitly configured in appsettings.json. If a key was missing, the feature failed at runtime even though a perfectly good Default connection existed.

Serenity now supports connection key fallbacks via the new IConnectionKeyFallbacks interface. DefaultConnectionStrings and DefaultSqlConnections implement it, and the fallback map is driven by assembly-level ConnectionKeyFallback attributes, so feature modules can declare their own logical connection key while still working in applications that only configure a fallback key:

// In a feature module (e.g. pro-features/src/pro.extensions):
[assembly: ConnectionKeyFallback(ProFeaturesConnectionKeys.ProFeatures, "Default")]

// In the Meeting module:
[assembly: ConnectionKeyFallback(MeetingConnectionKeys.ProMeeting,
    ProFeaturesConnectionKeys.ProFeatures)]

This defines a fallback chain like ProMeeting → ProFeatures → Default. When ProMeeting is not present in configuration, the next key in the chain that is configured is used.

A fallback can also be declared via configuration with Data:(ConnectionKey):FallbackFor, which accepts a semicolon separated list of connection keys. Config fallbacks override assembly attributes:

"Data": {
  "Default": {
    "ConnectionString": "...",
    "ProviderName": "System.Data.SqlClient",
    "FallbackFor": "ProFeatures;ProMeeting;ProWorkLog"
  }
}

The interface provides both directions of resolution:

public interface IConnectionKeyFallbacks
{
    // Ordered chain: the key itself + declared fallbacks
    IEnumerable<string> GetConnectionKeyFallbacks(string connectionKey);

    // First key in the chain that is actually configured (null if none)
    string ResolveConnectionKey(string connectionKey);

    // Reverse lookup: all keys that eventually resolve to the given key
    IEnumerable<string> GetConnectionKeysResolvingTo(string connectionKey);
}

Migrations for Fallback Connections

Migrations for fallback connections only run when TagBehavior.RequireAny is used. DataMigrations.cs in StartSharp (and Serene) now builds the migration tag list from the fallback chain, so a migration tagged [TargetDB("ProMeetingDB")] runs when ProMeeting resolves to Default via fallbacks:

options.Tags = (sqlConnections as IConnectionKeyFallbacks)?
    .GetConnectionKeysResolvingTo(databaseKey)?
    .Select(x => x + "DB").ToArray() ?? [databaseKey + "DB"];
options.IncludeUntaggedMigrations = databaseKey == "Default";

All StartSharp feature projects (Pro.Extensions, Meeting, WorkLog, OpenIddict, OpenIdClient, etc.) now use this feature instead of requiring every connection key to be configured.

SQL-Backed Distributed Cache (StartSharp)

A new SqlDistributedCache implementation of IDistributedCache (in Pro.Extensions) stores distributed cache entries in a SQL database table, useful for multi-server deployments where a Redis server is not available:

services.AddSqlDistributedCache(o =>
{
    o.ConnectionKey = "ProSqlDistributedCache"; // default
    // o.TableName = "DistributedCache";       // default
    // o.Prefix = ...;
    // o.DefaultSlidingExpiration = TimeSpan.FromMinutes(20);
    // o.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(30);
});

Related feature keys (SqlFileSystemMigrations, SqlDataProtectionMigrations, SqlDistributedCacheMigrations) were added so migrations for the FileSystem, DataProtectionKeys and DistributedCache tables can be optionally disabled via feature toggles — the corresponding migration classes are annotated with [RequiresFeature(...)].

DataProtection Improvements (StartSharp)

DataProtection configuration in Startup has been consolidated into a new DataProtectionSettings option type (section key DataProtection) and the AddDataProtectionWithSettings extension. The whole DataProtection setup in Startup.cs is now a single line:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddDataProtectionWithSettings(Configuration);
}

Everything else is configuration-driven:

"DataProtection": {
  "ConnectionKey": "Default",                     // persist keys in SQL via SqlXmlRepository
  "FolderPath": "App_Data/DataProtectionKeys",    // or persist to file system
  "ApplicationName": "StartSharp",
  "EncryptionKey": {
    "PrivateKey": "-----BEGIN PRIVATE KEY-----...", // PEM text or BASE64 DER blob
    "Password": "..."
  }
}
  • SQL key persistence: A new DataProtectionKeys migration and SqlXmlRepository type allow storing data protection keys in SQL databases via the PersistKeysToSqlConnection extension (used automatically when DataProtection:ConnectionKey is set). The table has FriendlyName and XmlData columns, and upserts are handled with the new SqlHelper.ExecuteUpsert.
  • EncryptionKeySpec: Provides more options to specify how to load an encryption key or certificate — from a BASE64 encoded PEM text or DER blob (PrivateKey), a file (CertificatePath, .pfx/.p12/.pem/.key/.der/.crt/.cer), or via a certificate thumbprint (CertificateThumbprint with CertificateStoreLocation / CertificateStoreName). Password is now used instead of the previous CertificatePassword / PrivateKeyPassword, and a better error is thrown when the password is incorrect.
  • IEncryptionKeyLoader / EncryptionKeyLoader: Handles loading the RSA key or certificate described by an EncryptionKeySpec.
  • ApplicationName option: If not set, the application name of IWebHostEnvironment or the entry assembly is used.

UPSERT Support in Fluent SQL

SqlInsert can now be converted to an UPSERT (INSERT or UPDATE depending on existence) statement:

var insert = new SqlInsert(DataProtectionKeysRow.TableName)
    .Set(nameof(DataProtectionKeys.FriendlyName), friendlyName)
    .Set(nameof(DataProtectionKeys.XmlData), xml);

// Executes dialect-specific UPSERT; falls back to a basic
// update-then-insert workaround for unknown dialects
insert.ExecuteUpsert(connection, [nameof(DataProtectionKeys.FriendlyName)]);
  • SqlInsert.FormatUpsert(tableName, fieldExpressions, keyFields, dialect) static method and ToUpsertString(keyFields) produce the UPSERT text for known dialects: SQL Server (NOT EXISTS ... UPDLOCK, SERIALIZABLE + IF @@ROWCOUNT = 0 UPDATE), Sqlite / Postgres (ON CONFLICT ... DO UPDATE), MySql (ON DUPLICATE KEY UPDATE), Oracle (MERGE) and Firebird (UPDATE OR INSERT ... MATCHING).
  • SqlHelper.ExecuteUpsert handles unknown dialects itself with a basic workaround, so callers don't need try/catch.

Query Introspection and Where Overload Removal (Breaking Change)

  • A new FieldExpressionPair readonly record struct (Field, Expression) is returned from the GetFieldExpressions method of SqlInsert and SqlUpdate (replacing the old Dictionary<string, string>-based Format overloads, which are now obsolete).
  • SqlUpdate gained GetWhereConditions() and GetWhereClause() methods.
  • [Breaking Change] The confusing and unused Where(params string[] conditions) overload was removed from SqlQuery and SqlUpdate. Call Where multiple times instead:
// Before:
new SqlQuery().Where("A = 1", "B = 2");

// After:
new SqlQuery().Where("A = 1").Where("B = 2");

Legacy overloads were also removed from DapperCore, with comments added to clarify its non-interception behavior.

Select2 Formatter Strings Are Now Text (Breaking Change)

[Breaking Change] Strings returned from Select2 formatters are now treated as text, not HTML markup. This prevents unescaped item text from being injected as HTML. If you specified any of the format options like formatResult, formatSelection etc. for select2 and returned HTML strings, you should modify them to return HTML elements / fragments instead:

// Before:
formatResult: (item) => '<b>' + item.text + '</b>'

// After:
formatResult: (item) => <b>{item.text}</b>

Internally, select2.ts was renamed to select2.tsx and containers are now created with JSX syntax, the deprecated e.which was replaced with e.key, and Select2.stripDiacritics now uses an alternative method with a smaller special case table.

UIDialogMaximizer Widget (dialogExtend Plugin Conversion)

The third-party jquery.dialogextend plugin was converted into a proper Serenity widget, UIDialogMaximizer, in @serenity-is/corelib:

import { DialogExtensions } from "@serenity-is/corelib";

DialogExtensions.dialogMaximizable(dialog);
// or manually:
new UIDialogMaximizer({ element: dialog.element[0] });

Options (UIDialogMaximizerProps) include dblclick (default true, double-click title bar toggles maximize) and showButton (default true). Related CSS changes make jQuery UI 1.13+ dialog close buttons look properly (close text hidden with proper alignment), including compat CSS adjustments.

UseNodeScriptRunner and Node Script Argument Parsing

A new UseNodeScriptRunner extension starts node scripts configured via the StartNodeScripts configuration key (semicolon separated script [args] entries) at application startup, and the NodeScriptRunner argument parsing was improved. pkgManagerCommand now defaults to node instead of npm, so entries like --run build execute via node --run:

public void Configure(IApplicationBuilder app, ...)
{
    // ...
    app.UseNodeScriptRunner();
}

StartSharp feature projects switched to it and removed their build:watch npm scripts.

Puppeteer-Specific PDF Options (StartSharp)

IHtmlToPdfOptions gained EditLaunchOptions and EditPdfOptions callbacks, which can only be used by the Puppeteer engine (they are ignored by other engines like WKHtmlToPdf):

var options = new HtmlToPdfOptions
{
    EditLaunchOptions = opt =>
    {
        if (opt is LaunchOptions launch)
        {
            // Puppeteer LaunchOptions (e.g. args, headless mode)
        }
    },
    EditPdfOptions = opt =>
    {
        if (opt is PdfOptions pdf)
        {
            // Puppeteer PdfOptions (e.g. PreferCSSPageSize)
        }
    }
};

Code Generation Improvements

  • DateTimeOffsetField generation: The RowFieldsSourceGenerator (Roslyn source generator) now supports DateTimeOffset? row properties, and tests were added for other field types (StartSharp).
  • OmitComments option: Source generator tests gained an OmitComments option, along with comment related tests (StartSharp).
  • Auto generated comments and pragma support in CodeWriter: Files generated by sergen and the generated ESM file now include XML doc comments / auto-generated warnings. *Columns.cs, *Row.cs, *Form.cs etc. were added to the ignore list for CS1591 missing XML comment warnings, with pragma warning restore at the end of suppressions. XML comment warnings are also suppressed in source generated row/interface files (StartSharp).
  • XML / JSDoc documentation pass: XML doc comments were added to core, services, web, extensions and feature projects (dataexplorer, pro.extensions, meeting, emailclient, openiddict, worklog), and JSDoc comments were updated for @serenity-is/corelib, @serenity-is/sleekgrid and @serenity-is/domwise. These comments are also the source of the new client/server API references on the docs site — see the Serenity Docs section below.
  • Change token support was added to type sources and cache property item processors, so cached property items can be invalidated when the underlying type source changes.
  • Root namespace constants: sergen now also generates a namespace constant for a root namespace even when there is no server type declared directly under it.

A New Serenity Docs Site

The Serenity documentation site at https://serenity.is/docs received a complete redesign, with a modern layout and dark theme support:

Docs Dark Theme Docs Light Theme

Reorganized and Expanded Framework Documentation

The table of contents was reorganized into two top-level sections — Backend Framework (.NET) and Frontend Framework (TypeScript) — and around 70 new framework topics were added.

New backend (.NET) topics include:

  • Data Access: Row Fields, Field Types, Entity Contracts, Annotation Types, SQL Data Manipulation, SQL Helpers & Settings, Query Extensions, Joins & Aliases, SQL Query Utilities, SQL Dialects, Entity CRUD & Query Helpers.
  • Services: Service Models, Retrieve Request Handler, Request Context, Service Behaviors (including Built-in Service Behaviors), Validation, Reporting, Uploads.
  • Web Layer: Navigation, Script Generation, Script & CSS Bundling, Web Security, Node Script Runner, JSON Serialization.
  • Sergen: a new Schema Provider topic alongside the existing code generator docs.

New frontend (TypeScript) topics include:

  • Core Functions: Signals and Reactivity, JSX with DomWise, Components and Hooks, Type Registration, Criteria.
  • Grids: DataGrid Architecture, RemoteView and Data Management, Creating and Configuring Grids, EntityGrid CRUD, Cell Editing, Filtering and Quick Search, Toolbar, Selection and Row Operations.
  • Script Classes: the Widget Class docs were rewritten, a PrefixedContext Class topic was added, and the obsolete ScriptContext / Widget With Options topics were removed.
  • Forms and Editors: Editors overview, Lookup Editors, PropertyGrid and Forms, Custom Editors.
  • Dialogs: EntityDialog and CRUD Workflows.
  • Integration: Type-Safe Service Calls, Authorization and Permissions, Data Binding, Localization and Text, Theming and CSS Customization, Performance, Config and Global Settings.
  • How-To Guides: Frontend Patterns Cookbook and Frontend Troubleshooting.

API Reference Generated from Source Comments

  • The client and server API references are now generated from the XML doc comments (C#) and JSDoc comments (TypeScript) added to the framework source in this release, so the API docs stay in sync with the code.
  • A new Serenity.Extensions API reference was added alongside Serenity.Net.Core, Serenity.Net.Services and Serenity.Net.Web.
  • Source links in the API docs are pinned to the last commit ID instead of master for consistency.

New StartSharp Feature Docs

Test Coverage Improvements

  • corelib, domwise and sleekgrid now have +90% test coverage, with new tests for editors, datagrid and dialogs.
  • Extensive new tests for sleekgrid: events, formatting, formatters, tree columns, draggable, column sorting/resizing, rendering, layout refs, group item metadata provider, basic layout lifecycle etc., plus a v8 coverage config for sleekgrid vitest.
  • import.meta.dirname is used instead of the obsolete __dirname in vitest configs.

Bugfixes

Serenity:

  • Fix issue where a server type generated at root folder imports a type from a sub folder and instead of ./Module/.., ../Module/.. is generated.
  • Fix several SleekGrid issues: frozen-end column filtering in FrozenLayout, guide top unit, getRowsRange descending order, column resizing guard, getScrollBarDimensions height, render-row pinned-end break, render-cell addAttrs handling, editors, and flashCell method when jQuery is not available.
  • Remove invalid closing tag from .select2-searching.
  • Fix date/time editors to respect min/max/sqlMinMax options, return after null in set_valueAsDate, and remove a stray double semicolon in timeeditor value getter.
  • Fix decimal/integer editors: preserve string format for AutoNumeric vMin, guard vMin against negative maxValue, and use abs maxValue for vMin when allowNegatives.
  • Fix radiobuttoneditor to preserve value during async enum load and support string enums.
  • Fix combobox/comboboxeditor: null-prototype item id map and abort init selection via correct property.
  • Fix servicelookupeditor to stop mutating the includeColumns option, and editorfiltering to stop re-adding cascadeFrom and default editor type to String.
  • Fix basefiltering to map combobox item text in getEditorText, editorutils to localize primitive boolean display text, and datetimefiltering le fall-through and ne boundary criteria.
  • Fix datagrid issues: clean up setInitialSortOrder and needsRefresh, cssEscape quick filter selector, simplify loop condition and quick filter check.
  • Fix grid row/radio selection mixins to use prototype-safe include maps and simplify selection toggle.
  • Fix slickpager to no-op on boundary page navigation.
  • Fix dialogs: correct icon closing tag in bootstrap button, guard dialog instance before responsive handling, and insert buttons correctly when no close button exists.
  • Fix propertygrid to throw Error for unloaded editors, and propertyitemcolumnconverter to show a warning icon when a lazy formatter fails.
  • Fix propertypanel to strip Panel suffix from form key after namespace.
  • Fix criteria to throw Error for mismatched parentheses.
  • Fix system helpers: guard getNested and throw Errors, correct hasOwnProperty guard for implemented interfaces, and preserve regex dotAll flag on deep clone.
  • Fix html helpers: tolerate malformed percent-encoding in parseQueryString and guard outerHtml against null element.
  • Fix services to bind header getter for 403 redirect, and scriptdata to build Lookup from resolved hook data with null-safe items access in getColumns/getForm.
  • Fix uploader to merge passed request into upload defaults and require container in createUploadInput; correct image editor css classes and dead branch in uploadeditors.
  • Fix htmlcontenteditor to apply readonly to the ckeditor instance, and dedupe interface / tidy extension filter in htmlcontenteditor-tiptap.
  • Fix filterpanel css class, fluent style guard with CSSStyleDeclaration check, and router onDialogOpen condition precedence.
  • Fix checktreeeditor bool check and null-prototype maps, and tabsextensions to guard null href and dispose all tab instances.
  • Fix emaileditor to validate user input on domain blur with readonly consistency.
  • Fix formatting-compat to parse day.hour:min with zero time and fix Turkish lowercase.
  • Fix jquery-compat to call isMobileView() for dialog focus handling, and basedialog to guard dialog instance.
  • Fix config to fall back to / for empty application path.
  • Fix debounce so flush returns the last result when there is no pending call.
  • Fix cascadedwidgetlink to remove no-op constructor bind and unbind from the stored parent node; dateyeareditor no longer mutates options when building the year list.
  • Fix quicksearchinput timer checks and option access, errorhandling dead filename fallback in script error report, and layout helpers to guard null element and non-finite height.
  • Fix widgetutils error message and symbol-safe id prefix proxy.
  • Fix corelib small-module bugs in formatters, blockui and enumeditor; null-guard issues in tree helpers and tooltip; toGrouping null-prototype lookup; layouttimer to throw Error and return number|null; tryGetText returns string | undefined.
  • Fix validator to throw Error and tidy rules/dataRules; remoteview to use null-prototype id/group maps; mocks to guard init.body and normalize response header.

StartSharp:

  • Fix application name ternary in DataProtection configuration.
  • Fix pro.coder tests.
  • Serenity submodule update for a migration running behavior fix.

Package Updates

Notable package updates in this release:

  • dompurify to 3.4.14, vitest to 4.1.11, es-module-lexer to 2.3.2, @tiptap packages to 3.30.5, @preact/signals to 2.11.1, datatables.net-bs5 to 3.0.3
  • PuppeteerSharp to 25.8.0, OpenIddict.AspNetCore to 7.6.1, WaffleGenerator.Bogus to 4.3.1

Upgrading to 10.4.0

  1. Update NuGet packages to 10.4.0.
  2. Where overload removal: If you called Where("A", "B") with multiple conditions on SqlQuery or SqlUpdate, chain separate Where calls instead: .Where("A").Where("B").
  3. Select2 formatters: If any of your formatResult / formatSelection etc. callbacks return HTML strings, change them to return HTML elements / fragments (see the Select2 section above).
  4. Connection keys: Feature module connection keys (e.g. ProMeeting, ProFeatures) no longer need to be configured if a fallback is declared. You may remove redundant connection entries, or declare config-based fallbacks via Data:(ConnectionKey):FallbackFor.
  5. DataProtection: If you configured DataProtection manually in Startup.cs, replace it with services.AddDataProtectionWithSettings(Configuration) and move settings to the DataProtection configuration section. If you used CertificatePassword / PrivateKeyPassword, rename it to Password.
  6. Encryption keys: If you loaded encryption keys/certificates with custom code, consider migrating to EncryptionKeySpec + IEncryptionKeyLoader.
  7. build:watch scripts: If your project relied on feature build:watch npm scripts, they were removed in favor of StartNodeScripts configuration consumed by app.UseNodeScriptRunner().
  8. Regenerate typings: Run sergen to regenerate server typings / rows so generated files pick up the new XML doc comments and the DateTimeOffsetField support.