Serenity 10.3.7 Release Notes (2026-08-12)

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

Simplified Service Registration with New DI Extensions

Many commonly-used services can now be registered with dedicated service collection extension methods, so your Startup.cs no longer needs to repeat the same lines for ClamAVUploadScanner, EmailSender, ElevationHandler, etc. All of these extensions return IServiceCollection, so they can be chained fluently.

New extensions added to Serenity and the common features:

Extension Registers
AddClamAVUploadScanner() IUploadAVScannerClamAVUploadScanner
AddElevationHandler() IElevationHandlerDefaultElevationHandler
AddEmailSender() IEmailSenderEmailSender
AddPasswordStrengthValidator() IPasswordStrengthValidatorPasswordStrengthValidator
AddHttpContextItemsAccessor() IHttpContextAccessor and IHttpContextItemsAccessor
AddCssAndScriptBundling() Both CSS and Script bundling in one call
AddLocalTextInitializer() ILocalTextInitializerDefaultLocalTextInitializer

The existing AddCssBundling(), AddScriptBundling(), AddUploadStorage() (and its Action<UploadSettings> overload) and AddLocalTextInitializer() methods now also return IServiceCollection instead of void, so they can be chained.

StartSharp additionally gained AddBackgroundJobs(), AddPuppeteerHtmlToPdf() (now returns IServiceCollection) and AddSecureUploadFileResponder().

// Before (StartSharp Startup.cs):
services.AddSingleton<IBackgroundJobManager, BackgroundJobManager>();
services.AddSingleton<IElevationHandler, DefaultElevationHandler>();
services.AddSingleton<IEmailSender, EmailSender>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IHttpContextItemsAccessor, HttpContextItemsAccessor>();
services.AddSingleton<IPasswordStrengthValidator, PasswordStrengthValidator>();
services.AddSingleton<IUploadAVScanner, ClamAVUploadScanner>();
services.AddSingleton<IUploadFileResponder, SecureUploadFileResponder>();
// ...
services.AddDynamicScripts();
services.AddCssBundling();
services.AddScriptBundling();
services.AddUploadStorage();
services.AddPuppeteerHtmlToPdf();
services.AddReporting();
// After:
services.AddBackgroundJobs();
services.AddClamAVUploadScanner()
    .AddSecureUploadFileResponder()
    .AddUploadStorage();
services.AddDynamicScripts()
    .AddCssAndScriptBundling();
services.AddEmailSender();
services.AddElevationHandler();
services.AddHttpContextItemsAccessor();
services.AddLocalTextInitializer()
    .AddAITextTranslation(Configuration);
services.AddPasswordStrengthValidator();
services.AddPuppeteerHtmlToPdf()
    .AddReporting();
services.AddServiceHandlers();

TranslationRepository Now Uses ILocalTextInitializer

In Serene, the static Startup.InitializeLocalTexts(IServiceProvider) method has been removed. Local texts are now initialized through the ILocalTextInitializer abstraction, which is registered via AddLocalTextInitializer() and invoked in Configure through the new app.InitializeLocalTexts() extension:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    RowFieldsProvider.SetDefaultFrom(app.ApplicationServices);
    app.InitializeLocalTexts(); // replaces Startup.InitializeLocalTexts(app.ApplicationServices)
    // ...
}

TranslationRepository also now resolves ILocalTextInitializer from the service provider to reload translations after a save, instead of calling the removed static method.

PrefixedContext Constructor Update (Generated Form.ts)

The PrefixedContext constructor now optionally accepts an object { idPrefix: string, domNode: HTMLElement } (e.g. a Widget) together with an optional second context: HTMLElement argument. This allows Form objects to keep working even when they belong to detached widgets such as dialogs, because lookups via byId() and w() are scoped to the widget's domNode instead of always searching the whole document.

Generated Form.ts files now use this constructor, and the legacy w0, w1 etc. aliases for editor classes have been removed:

// Before (10.3.5 and earlier):
export class CategoryForm extends PrefixedContext {
    static readonly formKey = 'Northwind.Category';
    private static init: boolean;

    constructor(prefix: string) {
        super(prefix);

        if (!CategoryForm.init) {
            CategoryForm.init = true;

            var w0 = StringEditor;

            initFormType(CategoryForm, [
                'CategoryName', w0,
                'Description', w0
            ]);
        }
    }
}
// After (10.3.6+):
export class CategoryForm extends PrefixedContext {
    static readonly formKey = 'Northwind.Category';
    declare private static init: boolean;

    constructor(...args: ConstructorParameters<typeof PrefixedContext>) {
        super(...args);

        if (!CategoryForm.init) {
            CategoryForm.init = true;

            initFormType(CategoryForm, [
                'CategoryName', StringEditor,
                'Description', StringEditor
            ]);
        }
    }
}

Migration Guide

When creating a form object, pass this (the widget) instead of this.idPrefix to benefit from context scoping:

// Before:
new CategoryForm(this.idPrefix);

// After:
new CategoryForm(this);

The string-based form still works (the constructor accepts a string prefix as before), so this is only a source-compatibility concern for code that overrode generated Form.ts files manually.

Stable Decimal Ordering for Dynamic Navigation Items

Navigation ordering now supports stable, decimal order values, which is especially useful for CMS-style modules that insert dynamic pages into the menu at runtime.

  • NavigationItemAttribute.Order and NavigationItem.Order are now decimal instead of int.
  • int.MaxValue is preserved as the "order not set" sentinel produced by the code generator.
  • Duplicate static orders are healed deterministically via the new NavigationHelper.HealOrders method: colliding siblings are spread out over the gap to the next distinct order, with the first one keeping its declared value.
  • Generated "unset" orders are anchored near their siblings (instead of near int.MaxValue) and spacing is based on the navigation depth.

Because orders are now decimal, a CMS can compute a midpoint between two siblings without renumbering everything:

var first = /* sibling before insertion point */;
var second = /* sibling after insertion point */;
newItem.Order = (first.Order + second.Order) / 2m;

RTL Support in SleekGrid

Several RTL (right-to-left) related improvements were applied to SleekGrid:

  • Changes from sleekgrid PR #16 were applied, and slick.base.css was adjusted so it works properly in RTL mode — including in the Pro theme, which includes that CSS and runs it through rtlcss.
  • A basic RTL SleekGrid sample was added to the demo project to make RTL behavior easy to test.
  • The unmerged SortableJS PR #2368 patch for RTL drag & drop support was manually applied.
  • Drag/resize constraints in RTL were corrected by swapping the shrink/stretch leeways (#7420), so columns can be resized correctly when they reach their minimum width.

FullCalendar 7 (StartSharp)

The Calendar sample now uses FullCalendar 7.0.2, which is distributed as a single unified fullcalendar package instead of the previous @fullcalendar/core, @fullcalendar/daygrid, etc. packages. This is a breaking change for code importing from the old package names.

// Before:
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import listPlugin from '@fullcalendar/list';
import timeGridPlugin from '@fullcalendar/timegrid';

// After:
import { Calendar } from 'fullcalendar';
import dayGridPlugin from 'fullcalendar/daygrid';
import interactionPlugin from 'fullcalendar/interaction';
import listPlugin from 'fullcalendar/list';
import timeGridPlugin from 'fullcalendar/timegrid';
import themePlugin from 'fullcalendar/themes/monarch';
import 'fullcalendar/skeleton.css'
import 'fullcalendar/themes/monarch/theme.css'
import 'fullcalendar/themes/monarch/palettes/blue.css'

Note that in FullCalendar 7 the theming is now opt-in: you must add themePlugin to the plugins array and import the theme CSS files (e.g. the Monarch theme shown above). The updated sample is in CalendarPage.tsx.

Internal X.PagedList Replacement (StartSharp)

The X.PagedList / X.PagedList.Mvc.Core NuGet packages used by the BootstrapForm paging sample have not been updated for some time. They were replaced with a minimal, single-file internal version of X.PagedList (kept in the sample module) that renders the same Bootstrap 5 pagination markup and contains only the members actually used by the sample. No change in behavior for the sample itself.

Service Typings Now Ignore CancellationToken Parameters

Service endpoint methods that take a CancellationToken previously produced no client proxy at all, and the failure was silent — the method simply did not exist on the generated TypeScript service. IsPublicServiceMethod already filters out interface parameters (IDbConnection, IUnitOfWork) and [FromServices] ones, but a CancellationToken is a struct with no attribute, so it survived the filter and pushed a one-request method to two parameters.

CancellationToken parameters are now excluded exactly like the other framework-injected parameters, so endpoints taking one now get a proper client proxy:

[HttpGet, ServiceAuthorize]
public async Task<ListResponse<MyRow>> List(
    IDbConnection connection, // excluded (interface)
    ListRequest request,      // request parameter -> part of the proxy signature
    CancellationToken cancellationToken) // now excluded too
{
    // ...
}

This fix applies to both sergen and the Roslyn source generator, which share the same code.

Security Fix: isSafeReturnUrl Rejects Protocol-Relative URLs

A security issue (CWE-601, reported as GHSA-9qr4-c5fv-3vwm) was fixed in isSafeReturnUrl. The previous check only required a leading slash, which a protocol-relative URL like //evil.example also satisfies, so it was reported as safe. getReturnUrl then returned it and callers assigned it to window.location.href, where the browser resolves it as an absolute URL on the current scheme — leaving the site right after a successful login (an open redirect).

isSafeReturnUrl now requires exactly one leading slash, so protocol-relative URLs are rejected. Same-site paths are unaffected; only inputs starting with two slashes change behavior:

isSafeReturnUrl("/safe/path");            // true
isSafeReturnUrl("//evil.example");        // false (protocol-relative -> open redirect)
isSafeReturnUrl("https://evil.example");  // false
isSafeReturnUrl("/\\evil.example");       // false

Criteria Now Uses a TypeScript Namespace

The Criteria helper was converted from attaching members as properties/functions on the function object to a proper export namespace Criteria { ... }. This was needed because TypeScript 6/7 produce an incorrect .d.ts file for the previous pattern. The public API is unchanged, so existing code that calls Criteria.isEmpty(...), Criteria.join(...), Criteria.Operator, etc. keeps working:

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

const c = Criteria("FirstName").like("A%");
if (Criteria.isEmpty(c)) { /* ... */ }
const combined = Criteria.join(c, "or", Criteria("LastName").eq("Smith"));

Namespace Constants for Root Namespaces

sergen now also generates a namespace constant for a root namespace even when there is no server type declared directly under it. This makes generated typings more consistent when a root namespace only contains nested namespaces (e.g. empty "container" namespaces used for organization).

Scriban Templates: Use IMPORTFROMCORELIB

The code generator's Scriban templates now use the unified IMPORTFROMCORELIB function when importing types from @serenity-is/corelib, instead of the older QIMPORT, SERENITYIMPORT, etc. functions. The old names still work for compatibility, but custom Scriban templates should be updated:

{{! Before: }}
export class {{ColumnsClassName}} extends {{SERENITYIMPORT "ColumnsBase"}}<...> {
    static readonly Fields = {{QIMPORT "fieldsProxy"}}<{{ColumnsClassName}}>();
}

{{! After: }}
export class {{ColumnsClassName}} extends {{IMPORTFROMCORELIB "ColumnsBase"}}<...> {
    static readonly Fields = {{IMPORTFROMCORELIB "fieldsProxy"}}<{{ColumnsClassName}}>();
}

Serene Application Improvements

mvct Generated Files Included in Compile Items

A bug was fixed where .cs files generated by sergen mvct during build (under Imports/) were not included in the Compile items, causing the build to fail the first time a new editor type etc. was defined. Serene.Web.csproj now explicitly re-adds these generated files to the Compile item group after running mvct.

noUncheckedSideEffectImports: false

Serene's tsconfig.json now sets noUncheckedSideEffectImports: false (matching the settings required by TypeScript 6/7). See the 10.3.5 release notes for the full list of required tsconfig.json settings.

Microsoft.TypeScript.MSBuild Removed

The Microsoft.TypeScript.MSBuild package reference was removed from Feature.Build.props and the sample Directory.Build.props. TypeScript compilation is handled by the tsbuild, so this MSBuild integration is no longer needed.

QuickSearchInput Also Listens to the input Event

QuickSearchInput now also listens for the input event in addition to change/keyup, because Playwright (and some browsers/automation) only trigger the input event. This keeps quick search behavior consistent when driven by UI automation.

Better Sidebar Markup Indentation (StartSharp)

The generated markup in _Sidebar.cshtml is now properly indented based on the navigation level, producing much more readable HTML output in the browser's developer tools. No functional change.

Package Updates

Notable package updates in this release:

  • vite to 8.2.1, esbuild to 0.28.2, jsdom to 30.0.1
  • typescript stays at 7.0.2; recommended @serenity-is/tsbuild is now 10.3.7 (uses esbuild 0.28.2)
  • dompurify to 3.4.13, preact to 10.29.8, @preact/signals to 2.11.0
  • @tiptap packages to 3.30.0, highlight.js to 11.11.2, datatables.net-bs5 to 3.0.1, @types/google.maps to 3.65.5 (StartSharp)
  • fullcalendar to 7.0.2 — unified package, see FullCalendar 7 section above (StartSharp)
  • SQLitePCLRaw.bundle_e_sqlite3 to 3.0.5, NUglify to 1.22.3, Scriban to 7.2.6, ClosedXML to 0.105.1
  • Microsoft.Data.Sqlite to 10.0.11, MySqlConnector to 2.6.2, System/ASP.NET Core packages to 10.0.11
  • PuppeteerSharp to 25.5.0, Microsoft.Playwright.Xunit.v3 to 1.62.0

Upgrading to 10.3.7

  1. Update NuGet packages to 10.3.7.
  2. Update npm packages: @serenity-is/tsbuild to 10.3.7+.
  3. Form object creation: If you create Form objects manually (rather than via generated code), consider passing this instead of this.idPrefix: new CategoryForm(this).
  4. Regenerate Form.ts / Columns.ts / etc.: For Serene apps, run sergen (or sergen mvct / servertypings) to regenerate typings so they use the new PrefixedContext constructor and IMPORTFROMCORELIB imports.
  5. Custom Scriban templates: Replace QIMPORT, SERENITYIMPORT, etc. with IMPORTFROMCORELIB when importing from @serenity-is/corelib (old names still work for now).
  6. FullCalendar (StartSharp only): If you consume the Calendar sample, migrate imports from @fullcalendar/* packages to the unified fullcalendar package and add the theme plugin + CSS imports (see the FullCalendar 7 section).
  7. Startup.cs (optional): Simplify your ConfigureServices with the new extension methods (see the DI Extensions section). If your project previously called Startup.InitializeLocalTexts(...), replace it with app.InitializeLocalTexts() and register AddLocalTextInitializer().