Serenity 10.3.5 Release Notes (2026-07-25)

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

EntityDialog Methods Now Return Promise (Breaking Change)

The EntityDialog methods save(), loadById(), load(), delete(), and undelete() have been updated to return a PromiseLike<T> instead of void. This is a breaking change if you have any code that overrides these methods and does not return a promise.

// Before (10.3.0 and earlier):
protected save(callback?: (response: SaveResponse) => void, initiator?: SaveInitiator): void | boolean {
    return ValidationHelper.submit(this.byId('Form'),
        () => this.validateBeforeSave(),
        () => this.save_submitHandler(callback, initiator));
}

protected saveHandler(options: ServiceOptions<SaveResponse>, callback: (response: SaveResponse) => void, initiator: SaveInitiator): void {
    serviceCall(options);
}

protected loadById(id: any, callback?: (response: RetrieveResponse<TItem>) => void, fail?: () => void): void {
    this.loadByIdHandler(this.getLoadByIdOptions(id, callback), callback, fail);
}

protected loadByIdHandler(options: ServiceOptions<RetrieveResponse<TItem>>, callback: (response: RetrieveResponse<TItem>) => void, fail: () => void): void {
    const request = serviceCall(options);
    fail && ((request as any)?.fail ? (request as any).fail(fail) : request.then(null, fail));
}
// After (10.3.5):
protected save(callback?: (response: SaveResponse) => void, initiator?: SaveInitiator): PromiseLike<SaveResponse> | false {
    if (!ValidationHelper.submit(this.byId('Form'), () => this.validateBeforeSave(), null))
        return false;
    return this.save_submitHandler(callback, initiator);
}

protected saveHandler(options: ServiceOptions<SaveResponse>, callback: (response: SaveResponse) => void, initiator: SaveInitiator): PromiseLike<SaveResponse> {
    return serviceCall(options);
}

protected loadById(id: any, callback?: (response: RetrieveResponse<TItem>) => void, fail?: () => void): PromiseLike<RetrieveResponse<TItem>> {
    return this.loadByIdHandler(this.getLoadByIdOptions(id, callback), callback, fail);
}

protected loadByIdHandler(options: ServiceOptions<RetrieveResponse<TItem>>, callback: (response: RetrieveResponse<TItem>) => void, fail: () => void): PromiseLike<RetrieveResponse<TItem>> {
    const response = serviceCall<RetrieveResponse<TItem>>(options);
    fail && ((response as any)?.fail ? (response as any).fail(fail) : response.then(null, fail));
    return response;
}

Additionally, new methods like getDeleteRequest() and getUndeleteRequest() have been introduced to better decouple request creation from the handler logic:

protected getDeleteRequest(): DeleteRequest {
    return { EntityId: this.entityId };
}

protected getUndeleteRequest(): UndeleteRequest {
    return { EntityId: this.entityId };
}

The IEditDialog interface has also been updated accordingly:

export interface IEditDialog {
    load(entityOrId: any, done: () => void, fail?: (p1: any) => void): PromiseLike<RetrieveResponse<any>>;
}

Migration Guide

If you override any of these methods in your derived dialog classes, you need to update the return type and ensure a promise is returned:

// If you override save():
protected override save(callback?: (response: SaveResponse) => void, 
    initiator?: SaveInitiator): PromiseLike<SaveResponse> | false {
    // Your custom logic
    return super.save(callback, initiator); // must return the promise
}

// If you override loadById():
protected override loadById(id: any, callback?: (response: RetrieveResponse<TItem>) => void, 
    fail?: () => void): PromiseLike<RetrieveResponse<TItem>> {
    // Your custom logic
    return super.loadById(id, callback, fail); // must return the promise
}

TypeScript 7 Migration

The project has been migrated to TypeScript 7.0.2. This builds on the TypeScript 6 migration from 10.3.0 and brings the latest TypeScript features. Key points:

  • dts-bundle-generator still uses TypeScript 6 as TS7 does not have the required API yet
  • Various code adjustments were needed for Vite 8 / rolldown / oxc compatibility:
    • Router namespace converted to a class (oxc does not support export let in namespaces)
    • Enum namespace converted to const (same reason)
    • SyntaxKind is now passed via constructor in TypeScript AST node classes instead of being set after construction
// tsconfig.json additions for TS7 (same as TS6):
{
    "compilerOptions": {
        "strict": false,
        "noUncheckedSideEffectImports": false,
        "noImplicitOverride": true,
        "rootDir": "."
    }
}

Domwise Fixes (10.3.5)

Several minor bug fixes have been made in the domwise library:

SVG Presentation Attributes

Fixed a bug where SVG presentation attributes were never removed on re-assignment because removeAttributeNS was called with swapped arguments. This affected SVG elements where attributes like fill, stroke, etc. were being updated.

// Before (broken):
element.removeAttributeNS(attrName, namespaceURI); // arguments swapped

// After (fixed):
element.removeAttributeNS(namespaceURI, attrName);

<select> Value with Signals

Fixed an issue where <select> element values were broken when using signals. The assignProp function now properly handles HTMLSelectElement (both single and multi-select), and the factory correctly unwraps signal values before assigning.

Show Component Memory Leak

Fixed a memory leak in the Show component where lifecycleNode was being set to non-EventTarget values (e.g., string children). This caused event listeners to never be cleaned up.

derivedSignal Fallback Crash

Fixed a crash in derivedSignal when the fallback value is used and subscribe does not fire synchronously on the first call.

Other Bug Fixes

  • unsignalizePrevClass: Now recursively handles nested arrays
  • Spellcheck cast: Fixed incorrect cast from HTMLInputElement to HTMLElement
  • getContent parameter type: Fixed parameter type in Show component

Custom Component Overload for jsx Function

The custom component overload for the jsx function has been restored. This allows TypeScript JSX transpilation to properly type-check custom components without needing a separate factory:

// The overload now accepts ComponentType:
export function jsx<P extends {}, TElement extends JSXElement = JSXElement>(
    type: ComponentType<P, TElement>,
    props?: P & { children?: ComponentChildren; ref?: Ref<TElement> } | null,
): TElement

This was previously removed and is now restored for compatibility with TypeScript 7 and custom component patterns.

Auto-Registration Attributes (C#)

Three new attributes have been introduced that allow an implementation type to be auto-registered for its implemented interfaces:

  • [RegisterScoped] — Registers as scoped lifetime
  • [RegisterSingleton] — Registers as singleton lifetime
  • [RegisterTransient] — Registers as transient lifetime
// Auto-register MyRepository for all its interfaces:
[RegisterScoped]
public class MyRepository : IMyRepository, IMyService
{
    // ...
}

// Register as singleton:
[RegisterSingleton]
public class MyCache : IMyCache { }

// Register with a specific service type (not all interfaces):
[RegisterSingleton(typeof(IMyRepository))]
public class MyRepository : IMyRepository, IMyService { }

IRequestHandler Exclusion

Request handlers (interfaces deriving from IRequestHandler) are excluded from auto-registration to avoid conflicts with the standard handler registration pipeline:

// This will NOT auto-register IRequestHandler implementations:
public interface IRequestHandler { }
public interface IRequestHandler<TRow> : IRequestHandler { }

Configuration

To enable auto-registration, add this line as the last line in your ConfigureServices method in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // ... your other registrations ...
    
    services.AddAutoRegisteredServices(); // should be the last line
}

Additional Options

Option Type Default Description
ReplaceExisting bool false If true, replaces any existing registration for the same service type
AsSelf bool false If true, also registers the type as itself in addition to interface registrations
Order int 0 Controls the order of registration when multiple implementations exist
// Replace an existing registration:
[RegisterSingleton(ReplaceExisting = true)]
public class CustomImplementation : ISomeService { }

// Register both as interface and as self:
[RegisterSingleton(AsSelf = true)]
public class MyService : IMyService { }

GridEditController Updates (StartSharp)

Several enhancements have been made to the GridEditController:

useGridEditorSave Renamed to useGridEditorServices

The option useGridEditorSave has been renamed to useGridEditorServices to better reflect that it controls both save and delete operations:

// Before:
new GridEditController({
    grid: myGrid,
    useGridEditorSave: true
});

// After:
new GridEditController({
    grid: myGrid,
    useGridEditorServices: true
});

New Options

new GridEditController({
    grid: myGrid,
    
    // Read-only mode check:
    isReadOnly: false, // or rely on grid.get_readOnly()
    
    // Permission checks:
    hasInsertPermission: () => myGrid.hasInsertPermission(),
    hasUpdatePermission: () => myGrid.hasUpdatePermission(),
    canEditItem: (item) => someCondition,
    
    // Delete permission (uses grid method if available):
    hasDeletePermission: () => myGrid.hasDeletePermission(),
});
  • isReadOnly: If not specified, checks the grid's get_readOnly() method or readOnly property
  • hasInsertPermission, hasUpdatePermission, canEditItem: Fine-grained control over edit operations
  • hasDeletePermission: Uses the grid's hasDeletePermission() method if available, falls back to the option

Auto-Register Function

The GridEditController now has an enableAutoRegister function for automatic registration:

// Auto-register GridEditController for all grids matching the pattern
GridEditController.enableAutoRegister(myGrid, options);

Async Operations

The controller's action handlers are now properly async, using await on save and delete operations:

protected async gridEditActionClick(e: MouseEvent) {
    // ...
    await this.saveChanges({ autoSavingItem: item });
    // ...
    await this.deleteRow(row);
    // ...
}

EntityGridDialog Missing Methods

The EntityGridDialog class (which mirrors EntityDialog for grid-based editing) was missing several methods that exist in EntityDialog. These have been added:

// Added methods:
protected async commitEdits(): Promise<boolean>
protected getDeleteOptions(callback): ServiceOptions<DeleteResponse>
protected getDeleteServiceMethod()
protected getUndeleteServiceMethod()
protected async retrieveLocalizations(): Promise<Record<string, Partial<TItem>>>

A new test was also added to catch any future missing EntityDialog prototype functions in EntityGridDialog, ensuring they stay in sync.

CSP Compatibility: Removal of javascript: URLs

All javascript: URLs in links have been removed as they are incompatible with strict Content Security Policy (CSP). They have been replaced with role="button" on anchor elements or other appropriate patterns:

<!-- Before (incompatible with strict CSP): -->
<a href="javascript:doSomething()">Click</a>
<a href="javascript:;" onclick="doSomething()">Click</a>

<!-- After (CSP-compatible): -->
<a role="button" onclick="doSomething()">Click</a>

Combo/Lookup Editor dialogType as Type or Promise

The dialogType option of combo/lookup editors now accepts a type reference or a promise in addition to a string:

// Before (string only):
new ComboboxEditor({
    dialogType: "MyModule.MyDialog"
});

// After (also accepts type or promise):
import { MyDialog } from "./MyDialog";

new ComboboxEditor({
    dialogType: MyDialog  // direct type reference
});

// Or with lazy loading:
new ComboboxEditor({
    dialogType: () => import("./MyDialog").then(m => m.MyDialog)
});

New createUploadInput Helper

A new createUploadInput helper function has been exposed, which creates a file upload input element and returns the underlying uploader object for more control:

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

const { uploader, input } = createUploadInput({
    uploadUrl: "/File/Upload",
    inputClass: "my-upload-input",
    done: (response) => {
        console.log("Upload complete:", response);
    }
});

Header Filters Fixes

getFilterValue Null Check (10.3.3)

Ensured HeaderFiltersPlugin.getFilterValue does not fail when the value is null or undefined:

// Before: could throw when value is null/undefined
// After: safely handles null/undefined values

Counter Text Cleanup

Removed \n and \t characters from <span class="counter"/> texts (which are used in login/2FA) to ensure display text is shown properly even when such characters are present.

Translation Validation (10.3.2)

The language ID is now validated before saving translations. Only valid ISO language codes (e.g., en-GB, tr-TR) are accepted:

// Invalid language IDs (e.g., typos, non-ISO codes) are rejected
// with an appropriate error message instead of silently failing.

Build/Tooling Changes

node --run Usage

The project now uses node --run scriptname instead of pnpm scriptname or npm run scriptname for running scripts defined in package.json. This is faster as it avoids the package manager overhead:

# Before:
pnpm build

# After:
node --run build

Vite 8 Migration

The build system has been migrated to Vite 8 (from Vite 5/6), which uses rolldown as the bundler and oxc as the transpiler. This brings significant performance improvements to the development and build workflow. Several code adjustments were needed:

  • Namespace exports (export let in namespaces) converted to class-based patterns
  • Test module specifiers updated for compatibility
  • enum namespaces converted to const objects

Tinyglobby

Replaced the glob package with tinyglobby for file pattern matching, which is faster and has a smaller dependency footprint.

isolatedDeclarations Enabled

The isolatedDeclarations compiler option is now enabled for domwise and sleekgrid packages, improving declaration file generation performance.

Package Updates

Updated NPM packages:

  • typescript to 7.0.2
  • @serenity-is/tsbuild to 10.3.2
  • vite to 8.1.5
  • @tiptap packages to 3.28.0
  • preact to 10.29.7
  • @preact/signals-core to 1.14.3
  • @preact/signals to 2.10.0
  • vitest to 4.1.10
  • esbuild to 0.28.1
  • jsdom to 29.1.1
  • dompurify to 3.14.2
  • es-module-lexer to 2.3.1
  • @floating-ui/dom to 1.8.0
  • tinyglobby to 0.12.7
  • sortablejs (no major update)
  • glob replaced with tinyglobby

Updated NuGet packages:

  • System/ASP.NET Core packages to 10.0.10
  • Microsoft.Data.SqlClient to 7.0.2
  • Microsoft.NET.Test.Sdk to 18.8.1
  • Spectre.Console to 0.57.2
  • NUglify to 1.22.0
  • System.IO.Abstractions.TestingHelpers to 22.2.0
  • MailKit to 4.17.0
  • Oracle.ManagedDataAccess.Core to 23.26.200
  • Npgsql to 10.0.3
  • Dapper to 2.1.79
  • MySqlConnector to 2.6.1
  • Scriban to 7.2.5
  • OpenIddict to 7.6.0
  • Markdig to 1.3.2
  • SixLabors.ImageSharp to 2.1.13
  • coverlet.collector to 10.0.0
  • Microsoft.Playwright.Xunit.v3 to 1.59.0
  • PuppeteerSharp to 25.3.4

StartSharp-Specific Features

Blog Application Sample

A new Blog application sample has been added under Application Samples, duplicating the functionality found at https://serenity.is/blog. This sample demonstrates:

  • File-based Markdown blog post rendering
  • A custom IBlogPostSource interface with auto-registration
  • File watching for hot-reload of blog content
  • Friendly URLs with relative link fixing
  • Integration with the auto-registration system via AddAutoRegisteredServices
// IBlogPostSource interface (auto-registered):
[RegisterScoped]
public class BlogPostFileSource : IBlogPostSource
{
    public async Task<IEnumerable<BlogPost>> GetPosts()
    {
        // Reads markdown files from the blog directory
    }
}

AddExceptionLogger Extension

The AddExceptionLogger extension now allows configuring an editSettings function, giving more control over how exception log entries are edited in the UI.

CSP Integration

All javascript: URLs have been removed (see CSP Compatibility section above), ensuring compatibility with strict Content Security Policy configurations.

Upgrading to 10.3.5

  1. Update NuGet packages to 10.3.5
  2. Update npm packages:
    • @serenity-is/tsbuild to 10.3.2+
  3. EntityDialog promise return: If you override save(), loadById(), delete(), undelete(), or load() in your dialog classes, update the return types to return a PromiseLike<T>:
    protected override save(callback?, initiator?): PromiseLike<SaveResponse> | false {
        return super.save(callback, initiator);
    }
    
  4. Rename useGridEditorSave: If you use GridEditController, rename useGridEditorSave to useGridEditorServices.
  5. Check GridEditController permissions: If you use permission-based editing, review the new hasInsertPermission, hasUpdatePermission, canEditItem, and hasDeletePermission options.
  6. Auto-registration: If you want to use the new [RegisterScoped], [RegisterSingleton], [RegisterTransient] attributes, add services.AddAutoRegisteredServices() as the last line in your ConfigureServices method.
  7. CSP compatibility: If you have custom links with javascript: URLs, replace them with role="button" or event-based handlers.
  8. TypeScript 7: Ensure your tsconfig.json has the required settings (same as TS6):
    {
        "compilerOptions": {
            "strict": false,
            "noUncheckedSideEffectImports": false,
            "noImplicitOverride": true,
            "rootDir": "."
        }
    }
    
  9. Review dialogType usage: If you pass dialog types as strings to combo/lookup editors, consider using direct type references or promises for better type safety.