Skip to main content

Version 4.1 release notes

Version 4.1.x release notes for the Lagoon framework, from newest to oldest. Versions marked ⚠ contain breaking changes. Other versions are listed on the Release notes page.

Version 4.1.1​

Patch release: startup of console (batch) applications on .NET 10 and minor dependency updates.

Bug fixes​

  • Batch: a console application no longer stops at startup with AmbiguousMatchException: Multiple properties or methods match the name GetVersion. The error occurred when building the host, before any command (--version included): on .NET 10 for any Main class deriving from LgApplication or LgApplication<TDbContext>, on .NET 8 only if the application overrode GetVersion(). This method remains overridable to customize the text displayed by --version.

Security​

No dependency vulnerability is fixed by this version: the version bumps below are functional fixes. The .NET 10.0.13 runtime remains required on servers (see the Security section of version 4.1.0). Applications that provide their own Chrome to Lagoon.PDF (Lagoon:Puppeteer:ExecutablePath) must keep it up to date (Chrome 155.0.8059.84 or later).

Dependencies​

  • Updates: Swashbuckle.AspNetCore 10.3.1 (notably fixes an AmbiguousMatchException on [FromQuery] parameters when generating the OpenAPI document), AWSSDK.S3 4.0.104.5.

Version 4.1.0 ⚠​

Minor version: LgListView overhaul (sorting, filtering, pagination, remote data, drag-and-drop), row validation and selection in LgGridView, ${Key} placeholders in configuration, users without database storage, lazy loading of Razor libraries, .NET 10.0.13 and security fixes.

LgListView: sorting, filtering, pagination, and remote data ⚠​

LgListView is rewritten on the LgGridView data pipeline: the list is fed by Items (local) or by a [Queryable] controller via ControllerUri (filtering, sorting, and pagination done on the server, business argument ControllerQueryArg, cancellation by the user with AllowCancellation). See the LgListView page.

  • Sorting: sortable fields are declared in SortOptions by LgListViewSort components whose SortSelector is a typed expression (SortSelector="@(x => x.Name)", like the SortSelector of LgGridView columns; the item type is inferred from the list). The sort key is the member path, or Key for a calculated expression; Text is optional (default: the [Display] of the member, then the key). The initial sort is the declaration marked IsDefault="true": DefaultSortProperty is removed, and without a default declaration, the list is not sorted at startup. OnSortChanged signals each change (Key, SortDirection, and Sorts in the same form as LgGridView). Sorting now also applies without grouping (it only applied to grouped lists before).
  • Filters: new Filters section with the LgListViewFilter component (FilterSelector="@(x => x.Status)", Key, Text, Items), rendered with the filter box of LgGridView corresponding to the member type (text, numeric, date, time, boolean, enumeration, value selection for other types); the "Selection" tab presents the values of the source filtered by other filters. Collapsible panel whose visibility is two-way bindable (ShowFilters, DefaultShowFilters) and which retains values when hidden, method ClearFiltersAsync. Like for LgGridView, OnFilterChanged provides active filters by key (Filters, HasFilters()) and is only triggered when they change, including when they are restored.
  • Pagination: AllowPaging, DefaultPageSize, PaginationSizeSelector (0 = "All"), CurrentPage / CurrentPageChanged, and like for LgGridView PaginationButtonCount, DisplayInputPageSelector, PagerOnTop and bindable counters ListViewDataCounter (ActiveRows, TotalPages, TotalRows).
  • Counters in the title: like for LgGridView, {0} is replaced in Title by the total number of items and {1} by the number of items on the displayed page (Title="Projects ({0})"), including in a translated title (#key). An unknown counter displays ... (before the first load, and for {1} when the page is empty); during a reload, the counters from the last load remain displayed.
  • Virtualization: Virtualize / VirtualizeItemSize, combinable with AllowPaging (items on the current page are virtualized). The list scrolls with the nearest scrollable ancestor (the document by default); to make it scroll itself, give .list-view-virtualized a height and overflow-y: auto.
  • Selection: like for LgGridView, it is enabled by SelectableRows; SelectedItems is renamed to Selection (ICollection<TItem>, created if omitted). With KeyField, selection is compared on a key property and survives item reload; without KeyField, a remote list uses the Id property of the model when it has one. OnRowSelection signals each change.
  • Persistence: StateId remembers in the browser's local storage the sorting, page size, filter values, filter panel state, and collapsed groups.
  • Default card: without ItemTemplate, each item is rendered in a configurable LgCard: BodyContent (body, item.ToString() by default), ColorSelector and StripeCssClassSelector (color or CSS class of the LgCardStripe), StripeIconSelector (icon of the stripe), StripeContent (content rendered in the stripe) and CssClassSelector (CSS classes of the card). LgCardStripe accepts child content (ChildContent) for this purpose. The stripe is rendered as soon as an item has a color, a class, an icon, or StripeContent, with the theme color by default if no color or class.
  • Clickable cards and active item: OnItemClick makes cards clickable and receives the clicked item; @bind-ActiveItem highlights the active item (class active, compared like selection), which a click on a card makes active. Per item: LinkSelector (page opened by the card), TooltipSelector / TooltipIsHtml (tooltip) and ConfirmationMessageSelector (confirmation before the click action).
  • Grid layout: ItemMinWidth (e.g., "18rem") arranges cards in a grid, with as many columns as the width allows (no effect with Virtualize).
  • Grouping: GroupSelector (x => x.Status) groups displayed items under collapsible headers (DefaultGroupCollapsed), sorted by GroupOrderSelector then by key, including on the server side with ControllerUri. The header displays the key and item count by default; GroupHeaderContent customizes it (ListViewGroupContext<TItem>: Key, Text, Items, Count, Color, IsCollapsed), GroupColorSelector adds a color stripe to it and, with SelectableRows, a checkbox selects items in the group (SelectGroup / UnselectGroup). With AllowPaging, groups are those of the current page. Like on a grouped LgGridView, two header buttons (and ExpandAllGroups() / CollapseAllGroups()) expand or collapse all groups. The new parameter GroupCollapsable (true by default) prevents, at false, collapsing groups: a group header has no collapse button and is no longer clickable, the "Expand all groups" / "Collapse all groups" buttons are not displayed, and groups remain expanded. See Grouping.
  • Header: the list header is no longer rendered when it is empty (no title, sorting, filtering, action bar, or selection).
  • ItemTemplate context: ItemTemplate receives a ListViewItemContext<TItem> instead of the item: Item (the item), Index (position among displayed items), IsSelected, Color and CssClass (results of ColorSelector and CssClassSelector), methods RenderStripe() (color stripe with StripeContent) and RenderBody() (div.card-body with BodyContent, or ToString() by default) to reuse the default card in a template, RenderCard(content) (the item's LgCard, with the click, active item, link, tooltip and confirmation, around free content), IsActive, ActivateAsync() (makes the item active without triggering OnItemClick) and ToggleSelectionAsync() to select the item from the template (like its checkbox). The context displays like the item (@context renders item.ToString()).
  • Messages: NoDataTitle, NoDataDescription and DataErrorTitle customize the "no data" and "load error" states; RefreshAsync() reloads the data.
  • Items is now of type ICollection<TItem> (a List<TItem> is still accepted). On the next parent render, the list reloads when the collection is replaced, when its item count changes, or after an ObservableCollection event, as well as on a ControllerUri or ControllerQueryArg change; outside of a parent render, call RefreshAsync().
  • Cached local data: with Items, the filtered and sorted result is calculated once then reused by page and page-size changes. It is recalculated when filters, sorting, grouping, or source change (different collection, different item count, ObservableCollection event). Like for the grid, an item modified in place is only taken into account after RefreshAsync().
  • An enumeration property is sorted by its value, not by its display name.

Automatic upgrade replaces SelectedItems with SelectableRows="true" Selection, converts <LgOptionListItem Value="@nameof(Project.Name)" Text="…" /> in SortOptions to <LgListViewSort SortSelector="@(x => x.Name)" Text="…" /> and moves DefaultSortProperty to IsDefault="true" on the same member's declaration. Non-convertible cases (Value calculated, DefaultSortProperty without matching declaration) are flagged by a LAGOON UNSUPPORTED marker.

Action

  • GroupBy (property name) is replaced by GroupSelector (typed expression x => x.Property) and GroupHeaderContent receives a ListViewGroupContext<TItem>: lists concerned are flagged by a LAGOON UNSUPPORTED marker.
  • Replace calls to SortListViewAsync(...) with RefreshAsync().
  • LgGridView: if you constructed a GridViewSortEventArgs, pass it the list of sorts (Sorts).

LgListView: header aligned with LgGridView ⚠​

The list is now rendered in an LgFrame, like the grid: its header has the same appearance (banner, title, button size, Kind) and the list can be collapsed. See Header: title, actions and collapse.

  • New parameters taken from LgFrame: IconName, Kind (default Kind.Primary), Collapsable, Collapsed / CollapsedChanged, DefaultCollapsed and TitleContent.
  • The action bar follows the grid's order: buttons from Toolbar, then "Filters" and "Clear", then the sort menu. It no longer uses size Large but that of frame toolbars (FrameToolbarButtonSize). Built-in buttons are hidden when the list is collapsed.
  • Sorting is chosen from a "Sort by: …" menu that offers SortOptions declarations then "Ascending" / "Descending" directions (grayed out while no sort is chosen). The direction arrow follows grid column headers: ↑ ascending, ↓ descending (it was reversed).
  • The "Filters" button takes icons from the grid; the "Clear" button is placed next to it when the panel is displayed (the panel's "Empty" button is removed).
  • New parameter ShowFiltersSummary: like in the grid, active filters are summarized under the title ("Label: value", at most two then "+N"), with the appearance of the grid summary in each theme. The summary is only displayed when the filter panel is hidden.
  • New parameter FiltersPosition (enumeration ListViewFiltersPosition): the filter panel displays above items (Top, default), in a centered modal window (Popup) or in a drawer to the left or right of the screen (Left, Right). When closed, the window is only hidden: filters keep their values. See Declarative filters.
  • New filter presentation, regardless of position: grid across full width (tinted banner above items), boxes shaped like dropdown lists (chevron), active filter highlighted with the theme's accent color, and a cross on each active filter to clear it alone. SCSS variables $list-view-filter-* and $list-view-filters-bg / -border-color / -border-radius to adapt it.
  • The pager and selection action panel are presented like those of LgGridView: size selector, pages and "go to page" field grouped on the right (with the alignment of each theme's grid footer, no border), action panel between items and pager. The pager no longer displays the total number of items (the grid doesn't): bind ListViewDataCounter to display it.
  • Pagers of LgGridView and LgListView: the page-size selector and "go to page" field now have the same height as page buttons (they were taller). This common height is calculated from the theme's $pager-item-* variables; the new $pager-control-height variable lets you set it.
  • List height: like for LgGridView, a height given by the style attribute (height or max-height) scrolls items in the list, with header, filters, action panel and pager remaining visible; the items area returns to the top on each page, sort or filter change. A virtualized list scrolls this way in the list without extra CSS. See Height and scrolling in the list.
  • Grouping: group headers become rounded banners (item count in a badge), tinted with the group's color with GroupColorSelector (color exposed as CSS variable --list-view-group-color), and remain stuck at the top of the scrolling area while you browse their group (outside virtualization). Group items are indented under its header ($list-view-group-indent).
  • LgCard active (Active, active item of an LgListView): the card is highlighted by a background tinted with the theme's accent color, a thin outline of the same color (its size no longer changes) and bold text; the tint is preserved on hover, and the active state remains visible outside hover (it only displayed on hover before). SCSS variables $card-active-bg and $card-active-border are replaced by $card-active-color (accent color) and $card-active-tint (tint intensity).
  • "Select all" becomes a checkbox in the header, before the title and aligned with item checkboxes: checked when all loaded items are selected, indeterminate for partial selection; a click on the checked checkbox deselects all.
  • Selection checkboxes are named by their item's content for screen readers.
  • Items take up the full width of the list (Custom, PF, TAS themes: they stopped 2rem before the right edge, even without checkboxes).

Action

  • Remove the LgFrame that may have wrapped an LgListView: the list is already a frame (otherwise frame in a frame). Move its title to the list's Title.
  • The CSS classes of the old header no longer exist (list-view-header, list-view-header-leftside, list-view-header-rightside, list-view-title, list-view-filter-action, list-view-sort-action, listview-sortprop, list-view-button-order, list-view-toolbar-action, list-view-filters-actions): adapt the CSS rules of the application that targeted them. CssClass and AdditionalAttributes now apply to the frame's root.
  • The SCSS variables $list-view-header-padding, $list-view-header-margin and $list-view-item-width are removed: remove them from the application's main.scss if it redefined them.
  • If the application's main.scss redefined $card-active-bg or $card-active-border, move the desired color to $card-active-color.

LgListView: drag and drop​

LgListView allows reordering its items and exchanging them between multiple lists, by mouse, touch, or keyboard. See LgListView — Drag and drop.

  • DragMode (flags ListViewDragMode): Reorder (in the list, no effect when the list is sorted), DragOut / DropIn (to / from other lists in the same DragGroup), Copy (source list keeps its items; copy created by ItemCopyFactory), All.
  • Items move by a handle displayed to the left of their card, before the color stripe; an ItemTemplate that doesn't use RenderCard places it with ListViewItemContext.RenderDragHandle(). On a touch screen, a long press anywhere on the card (outside fields, links and buttons) also starts the drag; a brief press remains a click and quick swipe scrolls the page. With DragFromItem, the mouse (or stylus) also moves the item from any point of its card, with the same exclusions: a click without movement remains a click; the handle remains displayed for keyboard.
  • An insertion line indicates the drop location (vertical in a grid list); a list without Reorder, sorted or empty is highlighted and receives the item at the end.
  • Item acceptance: the modes and group of the two lists, then the CanDrop predicate (ListViewDropContext<TItem>) of the target list, evaluated during hover (refused position: no line, card outlined in red), then OnItemDrop (ListViewDropEventArgs<TItem>) on release: Cancel refuses the drop, Handled indicates the handler updated the data.
  • Without Cancel or Handled, the list updates its own local Items (move, remove from source list, copy); data loaded by ControllerUri are not modified (the handler updates the source and calls RefreshAsync()). A list that cannot modify its items (remote, array…) accepts items from other lists only with an OnItemDrop handler. OnItemDropped is triggered once the item is in place.
  • Grouped lists: an item can change group when OnItemDrop is wired; the handler updates the grouped property from TargetGroupKey.
  • Keyboard: Space or Enter on the handle grabs the item, arrows choose the position, Space or Enter drops it, Escape cancels, Ctrl + arrow moves directly one position; positions are announced to screen readers.

An LgListView fed by local items no longer displays the loading indicator when it reloads (RefreshAsync(), item count change, sort, filter, page): displayed items are updated in place, preserving focus and avoiding flicker. Initial load and remote data (ControllerUri) keep the indicator. Rows are identified by KeyField (by Id for a remote list without KeyField, else by the item): an item moved or reloaded keeps its template's components instead of reusing those of the item displayed before at its position.

LgColorBar becomes LgCardStripe ⚠​

The LgCard's color stripe is renamed LgCardStripe (see LgCard), with its CSS classes (color-bar, color-bar-icon, color-bar-content become card-stripe, card-stripe-icon, card-stripe-content) and SCSS variables ($card-colorbar-* become $card-stripe-*, $notif-item-color-bar-bg-color and $notif-item-hover-color-bar-bg-color become $notif-item-card-stripe-bg-color and $notif-item-hover-card-stripe-bg-color). It accepts child content (ChildContent): it then takes the width of its content, with internal margin $card-stripe-content-padding.

Automatic upgrade renames the component (.razor, .cs) and CSS classes in stylesheets (.scss, .css).

Action

  • Rename SCSS variables overridden by the application ($card-colorbar-*, $notif-item-*color-bar-bg-color): they are not renamed by automatic upgrade.

LgGridView: sort event and data reload ⚠​

  • New event OnSortChanged: it provides complete sorting after user sorts or profile application (Sorts: sorted columns by sort order, with their direction; Key: main sort column). It is not triggered on initial load.
  • GridViewSortEventArgs now describes this complete sorting: its constructor receives the list of sorts; Key and SortDirection are kept and give the main sort column and direction.
  • The grid now reloads when an item is added to the same Items list (List.Add, without ObservableCollection or RefreshAsync()), on the next parent render.
  • A ControllerUri change alone now reloads the grid (only ControllerQueryArg was watched).
  • OnFilterChanged is no longer triggered again when active filters haven't changed (same value entered again, profile loaded with the same filters): as soon as a filter was active, each filter refresh triggered it.
  • GridViewDataCounterChanged is no longer notified except on counter change (not on each load).
  • The pager's "go to page" field brings an out-of-bounds number (0, negative, beyond the last page) back to the first or last page.

LgGridView: row key, KeyField and Id ⚠​

KeyField is now the key for both selection and row reload (ReloadRow). Without KeyField, a local grid compares items by equality, a remote grid by the model's Id property. See LgGridView.

  • Local grid (Items) without KeyField: ReloadRow finds the item in Items with type equality (Equals / IEquatable<T>) instead of the Id property, which is no longer required. If the model doesn't redefine equality, only the same instance is found: an item replaced in Items by a copy is no longer reloaded, without error (the row keeps the old item).
  • Remote grid (ControllerUri) without KeyField: the model's Id property serves as key, for row reload (ReloadRow, OnAfterUpdate with EditRefreshMode="ReloadGrid") as before, and now also for selection: a selected row remains selected after a reload or page change. Id takes priority over model equality (Equals / IEquatable<T>) and must uniquely identify the item: rows sharing an Id, or whose Id is not set, are selected together. The key follows a KeyField or data source change (a grid receiving its ControllerUri after the first render moves to Id).
  • Remote grid without KeyField whose model has no Id property: ReloadRow raises an explicit error (translated) before sending the PATCH / PUT instead of a technical error after sending, and OnAfterUpdate (ReloadGrid) finds the reloaded item with type equality (Equals / IEquatable<T>).
  • A row whose item is no longer found (removed from Items or missing from the server response) is no longer emptied: it keeps its item, and OnAfterUpdate receives the edited item instead of null.

Action

  • Add KeyField to local grids without KeyField whose model implements neither Equals nor IEquatable<T>, when an OnCommandClick returns RefreshMode = ReloadRow after replacing the item in Items with a copy.
  • Set KeyField on remote grids whose model has an Id property that doesn't uniquely identify the item: parent Id in a joined view, projection that doesn't set Id, or model whose equality (Equals / IEquatable<T>) relies on other properties.

LgGridView: row validation in Cell mode ⚠​

  • New event OnCellValueChanged, triggered on each cell value change, whether the value is valid or not (args.IsValid); a valid value is saved before triggering. When multiple rows share the same data, the handler designates related rows with args.RevalidateItems(i => ...), or all rows with args.RevalidateItems(): the grid revalidates them and signals their error cells, including for IValidatableObject errors and validation attributes. Non-displayed rows (another page, filter, virtualization, rows recreated by a reload) are validated when displayed. Without a handler, no other row is revalidated. See LgGridView.

    private void OnCellValueChanged(GridViewCellValueChangedEventArgs<MyItem> args)
    {
    args.RevalidateItems(i => i.GroupId == args.Item.GroupId);
    }
  • The error indicator now follows the validation of the entire row. An error on another field in the row (e.g., raised by IValidatableObject) is signaled on the concerned cell, including a read-only cell, without waiting for it to be edited. An error row keeps its indicator when it leaves display and returns (page, scroll, reload).

  • Modifying a column linked by an indexer or nested path updates other cells' errors in the row, and canceling an input (Escape) no longer makes errors still present on the row disappear.

  • Enter in a valid cell validates the input without submitting the row's form. It no longer launches a second validation of the entire row, which displayed error messages twice and also checked fields never entered.

  • When the server refuses to save a cell with validation errors (ValidationProblemDetails or ModelState, keys Data.<Field>), cells of refused fields are signaled on the row, including after ReloadRow, and the row is no longer saved while they are not modified. The LgRemoteModelStateException exception (validation errors returned by the server, property Errors) becomes public.

  • Each value change verifies validation attributes of all members of the edited row's item, like server validation of the saved item: a required field never entered is signaled (and the row is not saved) on the first row modification, instead of being refused by the server. Related rows only verify the modified field and fields already in error.

  • When an input is not saved because the row has an error on another field, the error message also lists errors in these fields (validation attributes, server errors), and no longer only the generic message "There are one or more errors on the row".

  • LgEditForm: new method RevalidateFields(fieldNames), which revalidates given fields as if they had changed, without displaying their errors in the toastr or changing their "modified" state (useful when the form's data was modified elsewhere).

  • The CellValidationState class and _cellValidationState protected field of LgGridCell are removed: validation state is now carried by the row.

Action

  • A cell derived from LgGridCell that used CellValidationState or _cellValidationState must read the row's state instead.

Configuration: secrets at design time and ${Key} placeholders ⚠​

  • At design time (dotnet ef), DesignTimeApplicationDbContextFactory<T> now loads, after appsettings.json and appsettings.Development.json, user secrets then environment variables, in the same priority order as at runtime. A connection string stored with dotnet user-secrets (or passed by ConnectionStrings__<Name>) is thus used by migration commands. Secrets are read with the UserSecretsId of the Model project: give it the same as the Server project, for example in a Directory.Build.props placed in the application folder.

    Directory.Build.props (application folder)
    <Project>
    <PropertyGroup>
    <UserSecretsId>00000000-0000-0000-0000-000000000000</UserSecretsId>
    </PropertyGroup>
    </Project>

    The new virtual extension point OnConfigureConfiguration(IConfigurationBuilder) lets you add or remove configuration sources. See Secrets and configuration placeholders.

  • ⚠ ${Key} placeholders in configuration. On the server, batch and at design time, a configuration value can reference another key, from any source (user secrets, environment variable, vault…). The connection string remains versioned and only the password is stored elsewhere:

    appsettings.json
    "ConnectionStrings": {
    "MainDb": "Server=db01;Database=App;User Id=app;Password=${DB_PASSWORD}"
    }

    Replacement is automatic (GetConnectionString, options, third-party libraries). ${Key ?? default} provides a default value and $${ a literal ${. A missing key without default raises an exception on read. In ConnectionStrings, an inserted value containing ;, ' or " is quoted. The Blazor client doesn't replace placeholders. "Lagoon": { "ExpandPlaceholders": false } disables replacement.

    Upgrade: lgn update escapes existing ${ in appsettings*.json (outside client) as $${. A value from another source (environment variable, vault…) containing ${ must be escaped by hand. DbContextHelper.GetExpandedConnectionString (syntax %VAR%) is removed (compilation error): use GetConnectionString. See Placeholders in configuration.

The "Forgot password" page sent the reset link to the address entered by the visitor, while the account is found by comparison on the normalized address: a different address (e.g., ſam@corp.com, with character U+017F, or the Kelvin sign instead of a K) would find the account sam@corp.com and receive its link. The link is now sent to the registered address of the account. Same pattern as CVE-2026-70175, which .NET 10.0.13 runtime does not fix for Lagoon (framework-specific page). The PF variant (LgPFLogin) is fixed the same way.

ILgAuthApplication.GenerateResetPasswordLinkAsync now returns a ResetPasswordLink (properties Url and Email, the account's registered address) instead of a string. It still returns null when no account matches.

ResetPasswordLink resetLink = await App.GenerateResetPasswordLinkAsync(typedEmail);
if (resetLink is not null)
{
await smtp.SendEmailAsync(resetLink.Email, subject, body.Replace("{0}", resetLink.Url));
}

Action: automatic upgrade flags each call or override of GenerateResetPasswordLinkAsync in the application (marker <-- LAGOON UNSUPPORTED) without modifying it. Send the email to resetLink.Email, never to the entered address, and use resetLink.Url as the link; an override returns new ResetPasswordLink(url, user.Email).

LgTextBox and LgNumericBox: OnInput on reset ⚠​

  • Reset by the clear button (✕) now triggers OnInput with an empty value (""), like the browser for a <input type="search"> cleared by its cross. A field that only listened to OnInput with a one-way bound Value (search field, filter) kept its old text until now after reset: empty field, filter still applied. OnInput is not triggered if reset is canceled (OnChangingAsync, refusal of the confirmation of ConfirmationMessage). The behavior is inherited by LgSuggestBox and LgColorPickerBox. See LgTextBox and LgNumericBox.
  • Also fixed: the "Columns…" window's search in LgGridView and the value list search in a date column's filter kept their filter after a click on ✕.

Action

  • A component that wired OnReset to the same handler as OnInput to work around the old behavior now receives two calls on each reset: remove the OnReset.

Users without database storage​

New option AuthenticationOptions.UserStorage. With UserStorageMode.None, SSO or Windows login no longer writes anything to the ASP.NET Core Identity tables (AspNetUsers, AspNetUserClaims, AspNetUserRoles, AspNetUserLogins, AspNetUserTokens), and OpenIddict no longer records codes and tokens (OpenIddictTokens). User identity (login, email, roles, custom claims) only lives in the authentication cookie and in tokens. Intended for applications that must not record any personal data.

Main.cs
protected override void OnConfigureAuthentification(AuthenticationOptions options)
{
options.UserStorage = UserStorageMode.None;
}
  • Roles are given at each login with context.AddCustomRole(...) in OnSignIn / OnSignInAsync.
  • User identifier (sub) is a random Guid on each login.
  • The cookie has an absolute expiration (SsoAuthenticationCookieLifeTime, 1 day by default): a role change at the identity provider applies at most at this deadline.
  • No refresh token is issued (offline_access is removed) and a token cannot be revoked.
  • Incompatible with Forms authentication and MFA (exception at startup), notifications, personal Remote grid profiles and any entity linked to the users table.

The default mode (UserStorageMode.Database) doesn't change. See Users without database storage.

Custom claims in tokens​

  • New option AuthenticationOptions.AccessTokenClaims: it declares claims emitted in the access token, for example custom claims (added with context.AddCustomClaim(...)) that the server needs for its authorization policies. A declared claim (case-insensitive comparison) is emitted in the access token and in the identity token, one claim per value, at login and at each access token renewal; it is also returned by /connect/userinfo. An API can thus be protected by a RequireClaim policy on this claim; the same policy, also declared in the client's OnConfigureAuthorization, works with AuthorizeView Policy="...".

    Main.cs (Server)
    protected override void OnConfigureAuthentification(AuthenticationOptions options)
    {
    options.AccessTokenClaims = new[] { "department" };
    }

    protected override void OnConfigureAuthorization(AuthorizationOptions options)
    {
    options.AddPolicy("Sales", policy => policy.RequireClaim("department", "Sales"));
    }

    When the list is filled, the access token contains only role claims, the displayname claim (used by change tracking to name the author) and declared claims: claims whose type ends with id must then be there to stay in the access token. Without AccessTokenClaims, nothing changes: the access token contains role, displayname claims and those whose type ends with id. The identity token is not affected: all user claims stay there. Each declared claim increases the Authorization: Bearer header size of all API calls: declare only those used on the server side. The ASP.NET Core Identity security stamp is never emitted, even if declared. See Policy based on a custom claim.

  • AuthenticationOptions.CustomClaims, without effect since 2.0, is marked obsolete: a claim added with context.AddCustomClaim(...) in OnSignIn / OnSignInAsync is available to the client application without being declared.

  • New option AuthenticationOptions.PreserveStoredClaims (false by default): with users stored in the database, login replaces claims in AspNetUserClaims with those added with context.AddCustomClaim(...); a claim recorded otherwise (admin screen, seeder…) is thus deleted, and all are if login doesn't add any (behavior since 3.32.1). With PreserveStoredClaims = true, only stored claims whose type is added at login are replaced: others are kept. See Claims stored in the database.

    Main.cs (Server)
    protected override void OnConfigureAuthentification(AuthenticationOptions options)
    {
    options.PreserveStoredClaims = true;
    }
  • Documentation: new section Display the user's name in the interface (displayname claim added at login, read on client side with GetUser().Claims.GetDisplayName()).

LgGridView "Columns" and "Groupings" windows​

  • The "Columns…" window displays two LgListView lists: displayed columns (reorderable by drag-and-drop or with Ctrl+Up / Ctrl+Down, pinnable, removable) and available columns, sorted by title with a search field. A column is added by drag-and-drop to the displayed list or with the "+" icon, and removed with the "×" icon or by dropping it on the available list; it slides from any point of its row, and from its handle on keyboard. Releasing a column outside these areas cancels the move.
  • Columns without a selection and commands title appear under the labels "[Selection]" and "[Action]".
  • The Groupings window is reviewed: grouped columns are displayed by level ("Level 1", "Level 2"…) in a list, without a dropdown list. A zone below the list lets you choose a groupable column (in grid column display order) and add it on a new level (at most 5 levels, counter displayed). A level can be merged with the previous level, moved up, moved down, or deleted; a column can be removed, placed on a new level, or dragged (mouse or keyboard) to another level to be combined with its columns. A "Clear all" button empties the groupings.
  • If profile saving fails (server unreachable, access denied, etc.), column and grouping changes remain applied to the grid.
  • The column and grouping options modals (LgColumnOptions, LgGroupOptions and their types ColumnOption, GroupOption, SaveColumnOptionsEventArgs, SaveGroupOptionsEventArgs) move from the Lagoon.UI module to the Lagoon.UI.GridView module, in the Lagoon.UI.Components.Internal namespace. These are grid internal components: an application that referenced them directly must add @using Lagoon.UI.Components.Internal (or using) and a reference to Lagoon.UI.GridView.

New features​

  • Lazy loading of Razor libraries (RCL): an RCL's assembly declared with the BlazorWebAssemblyLazyLoad element is downloaded only on first navigation to one of its pages. In OnConfigureBehavior, app.LazyAssemblies.Add("orders", "MyCompany.Orders.wasm") associates a route prefix with assemblies to load; for advanced cases, the new OnNavigateAsync(LgNavigationContext context) method of Main.cs is called before each navigation (context.LoadAssemblies(...), context.AddAssemblies(...)). LoadLazyAssembliesAsync preloads assemblies without navigation, and WindowManager.OpenModalAsync opens a deferred page in a modal. See RCL and lazy loading.

    protected override void OnConfigureBehavior(ApplicationBehavior app)
    {
    app.LazyAssemblies.Add("orders", "MyCompany.Orders.wasm");
    }
  • OpenIddict certificate in base64: the OpenIddict signing and encryption certificate can be provided as a string, without file, by the new key Lagoon:Authentication:CertificateBase64: convenient when secrets come from environment variables or a vault (containers). It accepts the compact form <certificate>.<private-key> (base64 of DER certificate and PKCS#8 private key, the shortest representation) or base64 of a pfx (password in Lagoon:Authentication:OpendIdCertificatePassword). It comes before CertificateFilePath and OpendIdCertificate, after CertificateThumbprint. CertificateLoader.FromBase64 / ToCompactBase64 (Lagoon.Server.Helpers) read and produce these strings for application-specific certificates. See Certificate.

  • LgGridView: the maximum number of sorted columns at once, until now fixed at 4, is adjusted with the new MaxSortedColumns parameter, or for the whole application with the global MaxSortedColumns option of app.GridView(...) (4 by default, behavior unchanged). If the user sorts one more column, the lowest-priority sort is removed. 1 limits the grid to a single sorted column, 0 removes the limit, a negative value raises an exception. See LgGridView and the global options.

  • LgGridView: the new ProfileFeatures parameter (or global ProfileFeatures option of app.GridView(...)) limits items saved in profiles and loaded from them: filters, sorts, position, visibility, freeze, column widths, groupings and page size (flags GridFeature, all by default; position and freeze go together). Unlike Features, an excluded item remains usable: it is not written to profiles and keeps its current state when a profile is loaded.

  • LgGridView: the new OnProfileLoadingAsync callback (Func<GridViewProfileLoadingEventArgs, Task>) is called and awaited just before a profile is applied, with the profile and the load reason (GridViewProfileLoadReason: initialization, user selection, current profile deletion). It can remove items from loading (LoadedFeatures) and choose whether non-loaded items keep their current state (default) or resume their default value (ExclusionMode). If it raises an exception, it is displayed and the profile loads completely. When current sorts are preserved, OnSortChanged is not triggered; when filters are, the filter row keeps the user's chosen state. See Profile content and partial loading.

  • LgGridSelectionColumn: new parameter CanSelectSelector (Func<TItem, bool>), which indicates for each row whether the user can select it. A refused row's checkbox is disabled and, on row hover, the checkmark is replaced by a crossed circle. Clicking the row (SelectableRows) doesn't select it and doesn't raise OnRowSelection (in SingleSelection, the previous selection is kept), and "select all" ignores it. A refused row already selected remains unselectable. The condition doesn't restrict code: SelectItem(), the collection linked to Selection and those in OnRowSelection arguments can add any item. See LgGridSelectionColumn. Icon, color and hover size are set in themes with $gridview-cell-selection-locked-hover-icon-name ('NoEntry', new alias IconNames.NoEntry), $gridview-cell-selection-locked-hover-icon-color and $gridview-cell-selection-locked-hover-icon-size (1rem).

    <LgGridSelectionColumn CanSelectSelector="@((OrderRow order) => order.Status != OrderStatus.Closed)" />
  • LgGridView: a selection modified in OnRowSelection is now displayed. Modifying args.Selection (Add, Remove, Clear) refreshes concerned rows (row class and checkbox), for example to deselect the previously selected item or to refuse the clicked item. Items are compared on the grid's key (KeyField, or Id on a remote grid): Remove also removes another instance of the same item, for example after a reload. args.Selection is no longer the instance of the collection linked to Selection, but a view of that collection: don't compare it to your collection and don't convert it to HashSet<T> / List<T>. See LgGridView.

  • LgGridView: new methods SelectItem(item), UnselectItem(item) and ClearSelection(), which modify selection and its display without triggering OnRowSelection, and RefreshSelection(), which updates the display after a direct modification of the collection linked to Selection.

  • LgGridCommandColumn: the new CommandsContent template declares commands of the menu for each row. Its context exposes the row's item (context.Item): an @if condition makes a command appear only on concerned rows. These commands are added after those of Commands and ChildContent, which remain common to all rows. A row's menu without commands is not displayed. Only LgGridCommand declarations are accepted in the template (an LgAuthorizeView raises a NotSupportedException). See LgGridCommandColumn.

  • Column parameters SortExpression, GroupExpression, FilterExpression, LambdaExpression and WeightExpression of LgGridView are now marked [Obsolete] and hidden from IntelliSense. They still work, but their use produces warning CS0618. Use instead SortSelector, GroupSelector, FilterSelector, LambdaSelector and WeightSelector, for example SortExpression="GetLambdaExpression((Person p) => p.Name)" → SortSelector="@((Person p) => p.Name)". See LgGridTextColumn.

  • Remote filtering "value in a list" of a [Queryable] controller (that of a RemoteListDataSource, or of a ModelFilter.AddIncludedInList / AddEquals) now accepts types char, DateOnly, TimeOnly and DateTimeOffset (and their nullable versions), in addition to strings, booleans, numbers, DateTime, enums, Guid and TimeSpan. For DateTimeOffset, equality is on the instant: 08:00 +02:00 equals 06:00 +00:00. The new method Filter.SupportsValueType(Type) indicates whether filters, and thus remote filtering of a [Queryable] controller, support a value type.

  • lgn update fixes .editorconfig and .gitattributes of applications: LF line endings for C#, .bat/.cmd scripts without BOM, default indentation (2 spaces for MSBuild, XML, JSON and YAML files), obsolete or duplicate settings removed. Customizations are kept. A .gitattributes is created at the solution root if missing.

Bug fixes​

  • ⚠ LgGridView's date column filter (LgGridDateColumn and month / year / week columns) now works on a DateOnly or DateTimeOffset value (or nullable), locally and on a remote grid: year / month / day selection only presented empty values or crashed, and the "From / To" rule produced incorrect dates. For a DateTimeOffset, the filter is on the displayed date, in the value's offset (like LgDateBox), not on the instant. Behavior change: a DateTimeOffset DateFilter<DateTimeOffset> range built by code (AddBetween, AddGreaterThan, AddLessThan) compared instants; it now compares displayed dates, and only the date of bounds counts (bounds' time and offset are ignored). To compare instants, use a NumericFilter<DateTimeOffset> (what ModelFilter.AddBetween creates): it is only usable in memory, remote filtering refuses it.

  • LgSelect / LgSelectMultiple fed by a RemoteListDataSource<TItem, TValue> whose value type is not scalar (a record, class or structure, e.g., vm => new MyKey(vm.Code)) displayed "Err!" instead of the label of initial values: the server doesn't know how to filter on this value type. Items are now loaded without filter and retained in memory by value equality (ValueEquals of the source). Scalar types remain filtered on the server side.

  • LgSelect / LgSelectMultiple fed by a RemoteListDataSource<TItem, TValue> whose remote item type differs from the linked type (usual case: a VmFirm view model for an LgSelect<Guid?>) now reload their list when ControllerQueryArg changes (cascading selects); they kept the list from the first load until now. An argument change during a load cancels the current request and relaunches the request with the new argument: the stale response is no longer displayed. After a ControllerQueryArg change or a ResetDataSource() call, the request is sent immediately only if the select needs the data (list open, load in progress, selected values whose label is still to be resolved); otherwise it is deferred to the next list opening. The selected value is not cleared when the argument changes: if it no longer belongs to the new list, reset it to null in the handler that changes the argument (modifying ControllerQueryArg is enough, without calling ResetDataSource()).

    private Task OnSelectedCountryChanged()
    {
    _myCar.FirmId = null;
    _firmsByCountryDataSource.ControllerQueryArg = _myCar.CountryId;
    return Task.CompletedTask;
    }
  • A select with asynchronous load open while resolving the label of its selected value remained stuck on the loading indicator until closing: the list is now loaded when resolution completes.

  • LgGridView: when an OnCommandClick handler modifies args.Item in place (same instance) keeping the default refresh mode (GridEventRefreshMode.StateHasChangeRow), or when GridEventRefreshMode.ReloadRow reloads a local grid (which finds the same instance), the row now displays the new values. Since 4.0.0, read-only cells kept the displayed text before the command until the next RefreshAsync(). Only the command's row is recalculated.

  • LgGridView: a column displayed from column options (e.g., a column DefaultVisible="false") keeps its width on page reload. A column made visible by a profile, especially a profile saved on the server (StorageMode="GridViewProfileStorage.Remote"), no longer displays with a width of 1 pixel. After manual resizing, a newly added column's width is also saved in the profile.

  • LgGridView: a default filter (DefaultFilter) deleted by the user no longer reappears on profile reload; DefaultFilter only applies to columns absent from the profile. As a result, a DefaultFilter added later to an existing column doesn't apply to users whose profile already records that column. The "Reset" button in the columns menu now restores DefaultFilter instead of clearing all filters.

  • LgGridView: an item replaced in an ObservableCollection linked to Items (same collection, same item count) now displays again on the next render; the grid reloaded its data but redisplayed the old item, kept by its local cache.

  • LgGridView: the Groupings window directly modified the grid's groupings during editing ("Cancel" didn't restore them), and a new grid render during editing could reset the window. Editing is now done on a copy, applied only on validation.

  • LgGridView: a list-type column (LgGridEnumListColumn, LgGridSelectMultipleColumn, LgGridTagBoxColumn…) is no longer proposed for grouping. Grouping on such a column made grid loading fail (InvalidOperation_IComparerFailed), including on profile reload or with the GroupBy parameter: these columns are now ignored and following levels renumbered.

  • LgGridView: an item model (ItemModel, or the type of items) whose class declares an indexer no longer makes grid display fail.

  • LgGridView: the message "The add or update row doesn't exist." (saving a row without an editing row) is now translated; it displayed as ##EditFormNullException(fr?).

  • The grouping window's column dropdown of LgGridView takes up full width again.

  • Date filter (LgDateFilterBox, date column filter in LgGridView): choosing a date in the calendar of the Condition tab (fields From / To) closed the filter without applying the date. The calendar of an LgDateBox now keeps focus on the field when clicked, which also fixes an LgDateBox placed in any dropdown list; clicking the field reopens the calendar.

  • LgFrame: the ButtonSize parameter is now applied to frame toolbar buttons; when set, the toolbar remained in Medium size. Without ButtonSize, FrameToolbarButtonSize applies as before.

  • Keyboard navigation: the Tab key no longer raises a JavaScript error (e is not defined), and focus remains in the open modal window again (the error interrupted this retention since version 4.0.0).

  • Tab mode: a tab restored on startup whose page no longer exists displays the "not found" page on activation, instead of remaining empty.

  • Themes: on reload of a page whose URL has multiple segments (e.g., /lg/icon-library), preloading the theme stylesheet no longer goes 404 under the current route (/lg/theme-xxx.css) and the console no longer displays the error "Refused to apply style … MIME type". Action: in the application's index.html, place any <link rel="preload"> (or other relative URL) after the <base href="{BASE_HREF}" /> element, otherwise it is resolved under the current route.

Security​

This version updates the following dependencies:

  • .NET 10.0.13 (October 2026 Patch Tuesday, 44 CVEs). Fixes are in the runtime and SDK, not in packages: install ASP.NET Core Runtime / Hosting Bundle 10.0.13 on all servers. It notably fixes two critical Kestrel request smuggling flaws (CVE-2026-88092, CVE-2026-70188), unauthenticated denials of service on forms and multipart (CVE-2026-70181, CVE-2026-70196) and in XML signature validation used by SAML authentication (CVE-2026-58280, CVE-2026-69651, CVE-2026-96958, CVE-2026-96959, CVE-2026-96960). Install SDK 10.0.113 / 10.0.402 on workstations and CI (SDK CVEs, and runtime embedded in Blazor WebAssembly clients, which must be republished); applications enabling HTTP/3 on Windows are exposed to CVE-2026-105016 (code execution, critical) until the runtime is updated.
  • Microsoft.AspNetCore.Components.WebAssembly.Server 10.0.13: fixes CVE-2026-102870 and CVE-2026-96230 (debugging tools, development workstations). This fix is carried by the package: upgrade the reference in your applications' Server project.
  • PuppeteerSharp 25.14.0 (Chrome 155, over 300 browser security fixes): applications using Lagoon.PDF with automatic browser download get the fixed version; those providing their own Chrome/Chromium (Lagoon:Puppeteer:ExecutablePath, Docker image) must update it.
  • MessagePack 3.1.12: fixes GHSA-7rj2-5w9w-r9jx (process termination by a forged LZ4 stream). Lagoon is not directly exposed, but an application deserializing MessagePack received from its clients with LZ4 compression was.
  • System.Security.Cryptography.Xml / System.Security.Cryptography.Pkcs 10.0.13: Lagoon.Server and Lagoon.Services now reference them directly, replacing vulnerable versions pulled by DataProtection, OpenIddict and MimeKit and making NU1902 / NU1903 warnings disappear from NuGet audit in your projects. The effective fix remains from runtime 10.0.13. If your Model project references Microsoft.Data.SqlClient, add System.Security.Cryptography.Pkcs 10.0.13 to it to lift the last warning.
  • HTML editor (LgHtmlEditor): Jodit 4.18.2: fixes three XSS flaws in the editor published in September and October 2026 (GHSA-gr86-hxq8-g84q, critical: filter bypass via <noscript>; GHSA-7pxg-rf9q-fg33: stored HTML executed before sanitization; GHSA-jhhp-r3r7-v2cg: HTML dropped by drag-and-drop). Content dropped by drag-and-drop from outside the editor now goes through paste processing: it is sanitized like pasted content, the HTML paste dialog appears if needed, and OnPasteAsync is also called for this content. Jodit's sanitization remains on the browser: sanitize on the server (LgHtmlSanitizer) HTML content shared between users.
  • "Forgot password" page: see the Password reset link sent to registered address section above.

Dependencies​

  • .NET packages (Microsoft.AspNetCore.*, Microsoft.Extensions.*, Microsoft.EntityFrameworkCore.*, Microsoft.JSInterop.WebAssembly, System.DirectoryServices.AccountManagement) aligned to 10.0.13 (October 2026 Patch Tuesday). Action on servers and workstations (runtime / SDK) is described in the Security section.
  • Updates: AngleSharp 1.8.5, MessagePack 3.1.12, OpenIddict 7.7.2, PuppeteerSharp 25.14.0 (Chrome 155), Swashbuckle.AspNetCore 10.3.0, AWSSDK.S3 4.0.104.4, Azure.Storage.Blobs 12.30.1, Jodit 4.18.2.
  • OpenIddict 7.7.2: on .NET 10, the registration of the OpenIddict purge task no longer overwrites Quartz.NET options configured by the application.
  • Swashbuckle 10.3.0 changes the nullability representation (anyOf / allOf / oneOf, type: null): generated OpenAPI documents may differ, to verify if clients are generated from your Swagger.
  • Lagoon's copy of the Blazor authentication script (AuthenticationService.js) is aligned to version 10.0.13 of Microsoft.AspNetCore.Components.WebAssembly.Authentication (script unchanged, new cache number ?v=10.0.13).

Action: applications whose Microsoft.AspNetCore.* / EF Core references follow 10.* (set by the 4.0 migration) get versions 10.0.13 on restore; those pinning a version upgrade it to 10.0.13, especially Microsoft.AspNetCore.Components.WebAssembly.Server in the Server project, whose security fix is carried by the package. Also update the local dotnet-ef tool (dotnet tool update dotnet-ef).