Skip to main content

Release notes 4.0

Release notes for the Lagoon framework 4.0.x versions, from most recent to oldest. Versions marked ⚠ contain breaking changes. The other versions are listed on the Release notes page.

Version 4.0.1 ⚠​

Patch release: SAML2 authentication hardening and support for multiple identity providers, Jodit 4 HTML editor and server-side EULA sanitization, dependency security fixes.

SAML2 authentication ⚠​

SAML2 response parsing is hardened. The following checks are now always applied:

  • the signature must cover the node that contains it, with a unique identifier (protection against "signature wrapping");
  • the response status must be a success, and the response must contain only one assertion;
  • the assertion must carry a bearer confirmation with a delivery deadline (NotOnOrAfter) and, under RequireDestination, the application's Recipient;
  • only signed values satisfy the checks: the Destination and InResponseTo of an unsigned envelope are no longer sufficient;
  • SHA-1 signatures and transforms not defined by SAML are rejected;
  • documents containing a DTD are rejected, and the decompressed size (3 MB) and document depth are bounded.

The authentication request sets a correlation cookie: the response is accepted only by the browser that sent the request. Over HTTP, the application posts the response back to itself, adding one round trip.

Identity provider metadata must be downloaded over HTTPS (RequireHttpsMetadata). It is kept for 6 hours per location. If a new download fails, the previous version remains in use for IdpMetadataFallbackDuration (1 day by default), then authentication fails until the next successful download.

Attribute values typed as string, regardless of the prefix (xs:string, xsd:string from Shibboleth/OpenSAML…), and all AttributeStatement elements in the assertion are now read.

The relay state is bound to the authentication scheme name: authentications started before the update must be restarted.

Multiple SAML2 schemes are supported, each with its own identity provider:

  • the new CallbackPath option (/saml by default) gives each scheme its callback path and metadata URL; paths are checked at application startup;
  • the new AllowedNameIdDomains option restricts the user domains that an identity provider can authenticate: accounts are looked up by email, regardless of the scheme. IsAllowedLogin applies the same rule in a custom account lookup;
  • an assertion is identified by its issuer and identifier in the replay-prevention cache;
  • metadata shared by multiple schemes respects each scheme's fallback duration.

Action:

  • An identity provider that signs with SHA-1 is rejected: switch it to SHA-256, or set options.Validation.AllowSha1 = true.
  • Metadata downloaded from an http:// URL is rejected: switch to https:// or a local file, or set options.RequireHttpsMetadata = false for a development identity provider.
  • Code that reads a response directly with SamlResponse (or SamlResponse.FromHttpRequest) without ForRequest(…) now fails: provide the request identifier and callback URL with Validation.ForRequest(requestId, acsUrl), or explicitly disable RequireDestination / RequireInResponseTo.
  • ⚠ An application that already declares multiple SAML2 schemes (the extra ones were non-functional) will no longer start: give each one except one its own CallbackPath (for example /saml-partner), and register the new URL with its identity provider. An application with a single scheme needs no changes.
  • With multiple identity providers, set AllowedNameIdDomains on those that do not manage all application users.

See SAML2 authentication for details on the checks.

HTML editor and EULA: security fixes ⚠​

LgHtmlEditor upgrades from Jodit 3.24.5 to Jodit 4.15.14, which fixes several editor XSS vulnerabilities (HTML paste, source mode, javascript: links). The Jodit 4 AI assistant (the "AI Commands" and "AI Assistant" buttons) is disabled. Date components (LgDateBox, LgMonthBox, LgWeekBox, LgYearBox) no longer load the Jodit script, which they did not use.

The new server-side LgHtmlSanitizer service (Lagoon.Server) sanitizes HTML content: it removes scripts, event handlers, dangerous URLs, forms, and iframes. Lagoon applies it to EULAs, both on write (SetEula) and read (GetAllEula, including shared-mode files), without modifying the database. Clients re-download the cached EULAs once, without revalidation.

Action:

  • The LgEulaManager constructor takes an LgHtmlSanitizer parameter: update any code that instantiates it directly or derives from it. A custom ILgEulaManager implementation must sanitize content itself.
  • Check the rendering of your .htm EULAs: style elements and the class attribute are kept, but meta, title, script, and forms are removed.
  • Sanitize server-side any other content entered with LgHtmlEditor before displaying it to other users.

See Server-side HTML sanitization and EULA.

Bug fixes​

  • A Prefix / Suffix without an explicit PrefixType / SuffixType is again displayed as text: since 4.0.0, it was interpreted as an icon name (Suffix="kg" displayed nothing). Affects input fields and grid columns that expose these parameters.

Security​

This release updates the following dependencies:

  • PuppeteerSharp 25.12.0 (Chrome 154, more than 100 browser security fixes): applications using Lagoon.PDF with automatic browser download retrieve the fixed version; those providing their own Chrome/Chromium (Lagoon:Puppeteer:ExecutablePath, Docker image) must update it.
  • MessagePack 3.1.10: fixes GHSA-qhrr-8q5h-9q3h (CVE-2026-92707, memory amplification when deserializing to object) and GHSA-fx6j-x9m5-vmrf (ReDoS with AllowAssemblyVersionMismatch). Lagoon is not directly exposed, but application SignalR hubs accepting object parameters were.
  • HTML editor (LgHtmlEditor): Jodit 4.15.14: fixes XSS and prototype pollution vulnerabilities published in 2026 (including GHSA-rxcw-mc6f-6hr3 and GHSA-w3xv-x3fm-59ph).
  • Server-side EULA sanitization: see the HTML editor and EULA section above. LgHtmlSanitizer relies on HtmlSanitizer 9.2.1039 and AngleSharp 1.8.2, which fixes GHSA-cgp3-27rh-pcp2 (mXSS on serialization, not exploitable with the default LgHtmlSanitizer configuration).
  • .NET 10.0.12: aligned with the September 2026 Patch Tuesday. Fixes are in the runtime and SDK, not in packages: install the ASP.NET Core Runtime / Hosting Bundle 10.0.12 on servers (especially under IIS, CVE-2026-69304) and SDK 10.0.112 / 10.0.401 as well as an up-to-date Visual Studio on workstations and CI (CVE-2026-58649, CVE-2026-69806, CVE-2026-69439, CVE-2026-71328, CVE-2026-69522). Applications using SAML authentication from Lagoon.Server must also ensure their servers are not running a runtime earlier than 10.0.10 (July 2026 System.Security.Cryptography.Xml patches).

Dependencies​

  • ASP.NET Core, EF Core, and Microsoft.Extensions.* packages at 10.0.12; OpenIddict 7.7.1; Microsoft.IdentityModel.* 8.23.0; MailKit 4.18.1; AWSSDK.S3 4.0.103.4.
  • The Lagoon copy of the Blazor authentication script (AuthenticationService.js) is aligned with version 10.0.12 of the Microsoft.AspNetCore.Components.WebAssembly.Authentication package, including for net8.0 applications.

Version 4.0.0 ⚠​

Major version: migration to .NET 10, grid rendering overhaul, OpenIddict 7, and new build chain without external tools.

Upgrade a 3.x application

Follow the Migrating from 3.x to 4.0 page: prerequisites, lgn update --allow-major --dotnet-fw command, list of manual actions, database migration, and publishing. The sections below describe each change. Action notes indicate what the automatic update does not do.

Platform and dependencies​

  • Applications target .NET 10 (net10.0, LangVersion 12). Modules are published for net8.0 and net10.0. .NET 6 is no longer supported.
  • Major dependencies: EF Core 10, OpenIddict 7 (since 4.10), MessagePack 3 (since 2.5).
  • The upgrade requires lgn 4.0.5 or higher and the --allow-major option: lgn update --allow-major --dotnet-fw.
  • appsettings.json is now optional at design-time (dotnet ef commands).

Build: lgn is no longer required to compile​

An application's resources (CSS compiled from SCSS, JavaScript bundle, icons, merged dictionaries, index pages) are generated by MSBuild tasks from the Lagoon.Build package. They replace the lgn build call and the source generator. A development machine or build agent no longer needs the lgn tool: only the .NET SDK is required. lgn remains useful for its other commands (new, update, …).

Release builds minify JavaScript with Terser: they require node.js (Terser is installed automatically on first use). CSS is minified in Release and remains readable in Debug (use the LagoonMinifyCss property to force one or the other).

Grid​

Direct cell rendering ⚠​

The grid now executes the <Columns> fragment only once, on a row model, before the first render. It then renders each cell directly from the column state using compiled accessors. The performance gain is significant, especially on large pages.

Consequence: the context variable in <Columns Context="item"> designates the model, not the displayed row. A column parameter that read it (DisplayFormat="@(item.IsLate ? "N0" : "N2")", HideContent="@item.Masked"…) is no longer re-evaluated per row. The failure is silent: the code compiles, but all rows display the model's value.

  • In CellContent / EditCellContent templates, read the row via the cell context, renamed cell: cell.Item for the row item, cell.Value for the bound value. Columns without a value (selection, command, edit, separator) also expose @context.Item.
  • For a parameter that varies by row, use its <Param>Selector variant (see below).
Revert to per-row rendering

There is no global option: the grid automatically reverts to the old rendering, executing <Columns> for each row, as soon as any visible column carries a value without an expression. This occurs when a value is passed via Value="…" alone, without @bind-Value or ValueSelector. Context captures then work as in 3.x, but without the direct rendering performance gain. Such a column is neither sortable nor filterable, except with a SortSelector / FilterSelector.

Action: the update converts simple cases to selectors. It marks templates that capture the <Columns> variable with LAGOON UNSUPPORTED. Review column parameters that still use it.

Row selectors​

Each column parameter that can vary by row has a <Param>Selector variant, a Func<TItem,…> delegate evaluated for each row:

ParameterSelectorColumn
HideContentHideContentSelectorall
DisplayFormatDisplayFormatSelector (display only)all
CommandNameCommandNameSelectorLgGridCommandColumn
CommandTooltipCommandTooltipSelectorLgGridCommandColumn
IconNameIconNameSelectorLgGridCommandColumn
PrefixPrefixSelectorLgGridNumericColumn
SuffixSuffixSelectorLgGridNumericColumn
MinMinSelector (edit only)LgGridDateColumn
MaxMaxSelector (edit only)LgGridDateColumn
<LgGridNumericColumn @bind-Value="item.Amount"
DisplayFormatSelector="i => i.IsLate ? "N0" : "N2""
PrefixSelector="i => i.Currency" />
<LgGridCommandColumn CommandNameSelector="i => i.CanValidate ? "Validate" : """ />

Rule: the selector wins. If provided, its per-row result is used; the static parameter only serves as a default value when no selector is set. CssClassSelector (additive) and CanEditSelector (combined with AND) keep their own semantics. At the grid level, RowCanEditSelector (Func<TItem, bool>) allows or forbids editing an entire row. On a grid with a calculation row, read selectors are called with a null item: a selector that reads the item must handle this case.

The update rewrites Xxx="@(context…)" as XxxSelector="@(context => …)", but only when the context is named context and the tag fits on one line. Other cases must be reworked manually.

Value and expression selectors​

These selectors are expressions (Expression<Func<TItem,…>>), not delegates: they feed value, sort, filter, grouping, and export. They must remain translatable server-side on a remote grid ([Queryable]). They are frozen once, at column load time.

  • ValueSelector (Expression<Func<TItem,TColumnValue>>): alternative to @bind-Value, without capturing the context variable. It provides a complete value (read, sort, filter, group, export). A column declares either @bind-Value or ValueSelector. A simple member path (i => i.Amount) remains editable and translatable server-side. A computed expression (i => i.Qty * i.Price) makes the column read-only and limited to local sort/filter.
  • SortSelector / GroupSelector / LambdaSelector replace SortExpression / GroupExpression / LambdaExpression, now obsolete. Setting a SortSelector (or a LambdaSelector) activates sort, and LambdaSelector also serves as the default filter key.
  • FilterSelector (filterable columns) replaces FilterExpression. Setting it activates filter and provides its key.
  • WeightSelector replaces WeightExpression: weight for the weighted average (CalculationType="Average").

A value-type key is automatically "unboxed" to remain translatable server-side.

<LgGridNumericColumn TItem="Order" TColumnValue="decimal"
ValueSelector="o => o.Quantity * o.UnitPrice" Title="Total" />
<LgGridTextColumn @bind-Value="o.Reference"
SortSelector="o => o.CreatedOn" GroupSelector="o => o.Customer" />

The update transforms a Value="@(context…)" without @bind-Value into a ValueSelector and renames *Expression to *Selector. A lambda written inline is kept, and the GetLambdaExpression(…) wrapper is replaced by the inline lambda. A form like @(GetLambdaExpression(…)), or an expression read from a variable, must be reworked manually. Without @bind-Value, add TItem="…" if the compiler requires it.

LgGridColumn split into LgGridTextColumn and LgGridCustomColumn ⚠​

LgGridColumn is removed and replaced by two components:

BeforeAfterWhen
<LgGridColumn @bind-Value="item.Name" /><LgGridTextColumn @bind-Value="item.Name" />The bound value is a string.
<LgGridColumn @bind-Value="item.Reference" /><LgGridCustomColumn @bind-Value="item.Reference" />The bound value is another type (business type, Guid, …).

LgGridTextColumn has only one type parameter (TItem, provided by the grid); LgGridCustomColumn<TItem, TColumnValue> keeps both. LgGridCustomColumn has no filter: the old LgGridColumn imposed a text filter on all values. The filter cell remains empty and any DefaultFilter is ignored. Use the type-specific column, or the FilterCellContent template. In return, LgGridCustomColumn applies DisplayFormat to IFormattable values.

The update converts columns. It relies on semantic analysis to find the actual type of bound values. A non-textual column incorrectly converted causes a cannot convert 'X' to 'string' error. Replace it with the specialized column (LgGridNumericColumn, LgGridDateColumn, LgGridBooleanColumn, LgGridEnumColumn, …) or with LgGridCustomColumn. A row type that could not be inferred leaves the marker LAGOON_SET_TItem_Here (error CS0246).

LgDynamicGridView removed ⚠​

LgDynamicGridView and its LgDynamicGrid…Column columns are removed. A grid whose column count varies becomes a typed LgGridView<TItem>. Columns are generated by a @foreach / @for loop in <Columns> and bound by indexer on the row (@bind-Value="@item.Absences[day]"). A populated instance is passed via ItemModel to set the column count. See Dynamic columns.

Action: no automatic conversion. Replace each LgDynamicGridView manually.

Column parameter cleanup​

Filter parameters are no longer declared on all columns. They are moved to the LgGridBaseFilterableColumn base, inherited only by columns that expose a filter.

ParameterDeclared from 4.0 on…No longer exists on…
AllowFilter, DefaultFilter, FilterExpression, FilterShowAllItems, FilterDisableSelection, FilterCellContent, MinSearchFilterCharacterfilterable columns (text, numeric, date, time, boolean, enum, select, tagbox, summary…)LgGridCustomColumn, LgGridColorColumn, LgGridImageColumn, LgGridCommandColumn, LgGridEditColumn, LgGridSelectionColumn, LgGridSeparatorColumn
MaxLengthtext and summary columnsall other columns
InputDisplayFormatLgGridNumericColumn (only cell to read it)all other columns, where it had no effect

Razor does not report an attribute that does not match any component parameter. A MinSearchFilterCharacter or MaxLength remaining on a column that does not declare it raises an InvalidOperationException when the grid renders. The update removes them from the affected columns.

Input mask and placeholder per column​

The InputMaskOptions class and the column parameter of the same name are removed ([Obsolete(…, true)]). Mask options are declared directly on the column, and each column exposes only what its editor can do:

ColumnParameters
Text (LgGridTextColumn)Placeholder, InputMask, InputMaskKind, InputMaskPlaceholder, AutoUnmask, ClearIncomplete (+ MaxLength)
SummaryPlaceholder (+ MaxLength)
TagBoxsame as Text, plus its own label parameters
Select, SelectMultiple, Enum, EnumListPlaceholder, InputMask, InputMaskKind, InputMaskPlaceholder
Numeric, Date, Month, Week, Year, TimePlaceholder
Custom, Image, Color, Boolean, Command, Edit, Selection, Separatornone

InputMaskPlaceholder remains distinct from Placeholder. It sets the character that fills empty mask positions: with # and the mask 99-99-99-99-99, the field displays ##-##-##-##-##. Placeholder remains the hint text for the empty field. It is available on all mask columns, as well as on LgTextBox, LgSelect, LgSelectMultiple, LgSuggestBox, and LgTagBox.

The mask applies only to the plain text mode of LgTextBox. LgSummaryBox and LgGridSummaryColumn thus lose their mask parameters: their editor is a multiline area, where the mask never had an effect. LgTextFilterBox follows the same logic: its InputMaskOptions property is replaced by separate parameters.

The FilterCellContent template is fixed: it crashed the rendering of any grid that used it. Its context is FilterContext, whose Filter property is readable and modifiable.

Action: the update marks each InputMaskOptions="…" with LAGOON UNSUPPORTED. The value can be built anywhere, so it is not automatically convertible: declare the separate parameters on the column.

Events and methods​

  • OnValueChange becomes OnValueChangingAsync (GridViewValueChangingEventArgs): the handler returns a Task. The update adapts the signature. Action: a bare return; is marked and must return Task.CompletedTask.
  • OnCellClick receives a typed GridViewCellClickEventArgs<TItem>.
  • JsResizeColumnsAsync and RefreshCssVariablesAsync are merged into AutoResizeColumnsAsync.

New features​

  • Row virtualization (Virtualize, VirtualizeItemSize): only visible rows are rendered, useful with large pages (PageSize = 0). It is disabled by default. Enable it per grid (Virtualize="true") or for the whole application:

    Main.cs (Client)
    app.GridView(options => options.Virtualize = true);

    It does not apply to a grouped grid. See Row virtualization.

  • Local data cache filtered and sorted between page loads.

  • Reusable column sets (LgGridColumnSet): a component can group columns and be placed in <Columns>. Columns can also be placed in an LgGroup or an LgAuthorizeView.

Input components​

Renames (applied by the update):

BeforeAfter
LgSearchNavigationLgNavigationBox (the Scrollable parameter disappears)
LgInputRadioGroup / LgInputRadioLgRadioGroup / LgRadio (Name → GroupName)
OnChanging (LgCheckBox, LgToggle)OnChangingAsync
ChangingEventArgsChangingItemEventArgs
<LgDateBox DateBoxKind="Week/Month/Year">LgWeekBox / LgMonthBox / LgYearBox
LgOptionListItem TItemValue="…"LgOptionListItem TValue="…"

New features:

  • LgPasswordBox: password input field.
  • LgWeekBox and LgMonthBox: input for a week or month, following the LgYearBox pattern (replace DateBoxKind).
  • CascadingTypeParameter on LgSelect and LgSelectMultiple: the value type is passed to child options, which no longer need to declare it.
  • Internal overhaul of LgSelect, dropdown lists, color picker, and input components (no API change), with many fixes (dropdown opening/closing, label placement, value restoration after confirmation cancellation).

Application​

  • Single loading screen: it remains displayed from the first render until the application is ready, instead of being followed by a second "Authorizing…" screen. Its text and the page title take the application name (setting App:Name). The update adapts index.html and renames the SCSS variables $auth-app-loading-* to $app-loading-*.
  • Customizable spinner: 17 loading animations to choose from, for the startup screen and LgLoaderView. The spinner and its colors are chosen in the client's main.scss ($spinner, $sp-color, $sp-color-alt, $sp-bg-color, $sp-size). The Loading spinner page presents the spinner gallery and generates the SCSS block.
  • EULA: acceptance checkbox on the terms of use page, and recording of acceptance date (new EulaAcceptances table).
  • OnConfigureHttpClient(IHttpClientBuilder, string): new method in the client Main class to configure the application's HttpClient.
  • New z-index scale (Bootstrap overrides) for dropdown and modal stacking.
  • Poppins font delivered as WOFF2.

Server and database​

OpenIddict 7 and Guid keys ⚠​

OpenIddict entities now always use a Guid key, regardless of the user key type: builder.UseOpenIddict<Guid>(). A UseOpenIddict() without arguments prevents the application from starting. The update rewrites the call and generates the LagoonMigrationV4 migration, adapted to each engine (SQL Server, MySQL/MariaDB, PostgreSQL, SQLite). This migration also covers the OpenIddict 7 schema, ASP.NET Identity keys reduced to 128 characters, and the EulaAcceptances table.

Action: review the migration before applying it (it carries a LAGOON UNSUPPORTED marker) and back up the database. When the OpenIddict key was a string, OpenIddict tables are cleared: all users must log in again. See Migrating from 3.x to 4.0.

Design-time context factory​

The application's DesignTimeDbContextFactory now derives from DesignTimeApplicationDbContextFactory<T>. The base class loads the appsettings*.json configuration and constructs the DbContextOptionsBuilder with the same Identity options as at runtime. This removes the PendingModelChangesWarning at startup. DesignTimeConfiguration.GetDevelopmentConfiguration() is obsolete (compilation error). The update rewrites the factory.

MySQL / MariaDB: Pomelo replaced by Microting​

Pomelo publishes nothing for .NET 10: the Pomelo.EntityFrameworkCore.MySql package is replaced by its fork Microting.EntityFrameworkCore.MySql. The fork only renames the assembly and namespaces. The generated SQL, model annotations (MySql:CharSet, MySql:ValueGenerationStrategy, …), and existing migrations are unchanged. The update replaces the PackageReference and using directives.

CollationProvider constants name the engine​

An engine can be served by multiple providers: the constants of CollationProvider now contain a stable engine name, not the provider assembly name.

Constant3.x value4.0 value
CollationProvider.SQLServerMicrosoft.EntityFrameworkCore.SqlServerSQLServer
CollationProvider.OracleOracle.EntityFrameworkCoreOracle
CollationProvider.MySQLPomelo.EntityFrameworkCore.MySqlMySQL
CollationProvider.PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQLPostgreSQL
CollationProvider.SQLiteMicrosoft.EntityFrameworkCore.SqliteSQLite
CollationProvider.MariaDBMariaDBMariaDB (unchanged)

DbContextOptionsBuilder.GetProviderName() returns these same names. The new DatabaseEngine.FromProviderName(providerName) method converts an EF Core provider name (DbContext.Database.ProviderName) to an engine name.

Action: code that compared one of these constants to Database.ProviderName must use DatabaseEngine.FromProviderName(…). A comparison like GetProviderName() == CollationProvider.MySQL remains correct.

Publishing: compression and static files ⚠​

The toolkit replaces the SDK's default compression formats: no compression at build, and Brotli only at publish. The SDK produced .gz at build and .gz + .br at publish. At publish, the original file of every asset that has a .br variant is deleted. On the reference application, the published wwwroot drops from 23.8 MB to 10.4 MB. Never deleted: files without a compressed variant (images, fonts) and those that the Lagoon runtime reads by their uncompressed name.

The server thus serves static files with endpoints.MapStaticAssets() instead of app.UseStaticFiles(). MapStaticAssets negotiates the compressed variant and handles ETags and cache headers. It only serves files present in the build manifest. The update makes the replacement in Main.cs. A UseStaticFiles with options, or a pipeline without a UseEndpoints block, is marked and must be adapted manually.

Action:

  • An application served over HTTP receives no static files anymore, because Brotli is negotiated only on a secure origin. Set LagoonDefaultCompression to false in the solution's Directory.Build.props: gzip variants are restored and uncompressed files kept.
  • The 3.x property DisablePublishCleaning causes a compilation error: remove it.
Directory.Build.props
<Project>
<PropertyGroup>
<LagoonDefaultCompression>false</LagoonDefaultCompression>
</PropertyGroup>
</Project>

See Static file compression for format details, the exclusion list, and verification steps.