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
bearerconfirmation with a delivery deadline (NotOnOrAfter) and, underRequireDestination, the application'sRecipient; - only signed values satisfy the checks: the
DestinationandInResponseToof 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
CallbackPathoption (/samlby default) gives each scheme its callback path and metadata URL; paths are checked at application startup; - the new
AllowedNameIdDomainsoption restricts the user domains that an identity provider can authenticate: accounts are looked up by email, regardless of the scheme.IsAllowedLoginapplies 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 tohttps://or a local file, or setoptions.RequireHttpsMetadata = falsefor a development identity provider. - Code that reads a response directly with
SamlResponse(orSamlResponse.FromHttpRequest) withoutForRequest(…)now fails: provide the request identifier and callback URL withValidation.ForRequest(requestId, acsUrl), or explicitly disableRequireDestination/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
AllowedNameIdDomainson 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
LgEulaManagerconstructor takes anLgHtmlSanitizerparameter: update any code that instantiates it directly or derives from it. A customILgEulaManagerimplementation must sanitize content itself. - Check the rendering of your
.htmEULAs:styleelements and theclassattribute are kept, butmeta,title,script, and forms are removed. - Sanitize server-side any other content entered with
LgHtmlEditorbefore displaying it to other users.
See Server-side HTML sanitization and EULA.
Bug fixes
- A
Prefix/Suffixwithout an explicitPrefixType/SuffixTypeis 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.PDFwith 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 withAllowAssemblyVersionMismatch). Lagoon is not directly exposed, but application SignalR hubs acceptingobjectparameters 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.
LgHtmlSanitizerrelies on HtmlSanitizer 9.2.1039 and AngleSharp 1.8.2, which fixes GHSA-cgp3-27rh-pcp2 (mXSS on serialization, not exploitable with the defaultLgHtmlSanitizerconfiguration). - .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.Servermust also ensure their servers are not running a runtime earlier than 10.0.10 (July 2026System.Security.Cryptography.Xmlpatches).
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.S34.0.103.4. - The Lagoon copy of the Blazor authentication script (
AuthenticationService.js) is aligned with version 10.0.12 of theMicrosoft.AspNetCore.Components.WebAssembly.Authenticationpackage, including fornet8.0applications.
Version 4.0.0 ⚠
Major version: migration to .NET 10, grid rendering overhaul, OpenIddict 7, and new build chain without external tools.
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,LangVersion12). Modules are published fornet8.0andnet10.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
lgn4.0.5 or higher and the--allow-majoroption:lgn update --allow-major --dotnet-fw. appsettings.jsonis now optional at design-time (dotnet efcommands).
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/EditCellContenttemplates, read the row via the cell context, renamedcell:cell.Itemfor the row item,cell.Valuefor 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>Selectorvariant (see below).
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:
| Parameter | Selector | Column |
|---|---|---|
HideContent | HideContentSelector | all |
DisplayFormat | DisplayFormatSelector (display only) | all |
CommandName | CommandNameSelector | LgGridCommandColumn |
CommandTooltip | CommandTooltipSelector | LgGridCommandColumn |
IconName | IconNameSelector | LgGridCommandColumn |
Prefix | PrefixSelector | LgGridNumericColumn |
Suffix | SuffixSelector | LgGridNumericColumn |
Min | MinSelector (edit only) | LgGridDateColumn |
Max | MaxSelector (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-ValueorValueSelector. 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/LambdaSelectorreplaceSortExpression/GroupExpression/LambdaExpression, now obsolete. Setting aSortSelector(or aLambdaSelector) activates sort, andLambdaSelectoralso serves as the default filter key.FilterSelector(filterable columns) replacesFilterExpression. Setting it activates filter and provides its key.WeightSelectorreplacesWeightExpression: 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:
| Before | After | When |
|---|---|---|
<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.
| Parameter | Declared from 4.0 on… | No longer exists on… |
|---|---|---|
AllowFilter, DefaultFilter, FilterExpression, FilterShowAllItems, FilterDisableSelection, FilterCellContent, MinSearchFilterCharacter | filterable columns (text, numeric, date, time, boolean, enum, select, tagbox, summary…) | LgGridCustomColumn, LgGridColorColumn, LgGridImageColumn, LgGridCommandColumn, LgGridEditColumn, LgGridSelectionColumn, LgGridSeparatorColumn |
MaxLength | text and summary columns | all other columns |
InputDisplayFormat | LgGridNumericColumn (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:
| Column | Parameters |
|---|---|
Text (LgGridTextColumn) | Placeholder, InputMask, InputMaskKind, InputMaskPlaceholder, AutoUnmask, ClearIncomplete (+ MaxLength) |
| Summary | Placeholder (+ MaxLength) |
| TagBox | same as Text, plus its own label parameters |
| Select, SelectMultiple, Enum, EnumList | Placeholder, InputMask, InputMaskKind, InputMaskPlaceholder |
| Numeric, Date, Month, Week, Year, Time | Placeholder |
| Custom, Image, Color, Boolean, Command, Edit, Selection, Separator | none |
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
OnValueChangebecomesOnValueChangingAsync(GridViewValueChangingEventArgs): the handler returns aTask. The update adapts the signature. Action: a barereturn;is marked and must returnTask.CompletedTask.OnCellClickreceives a typedGridViewCellClickEventArgs<TItem>.JsResizeColumnsAsyncandRefreshCssVariablesAsyncare merged intoAutoResizeColumnsAsync.
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 anLgGroupor anLgAuthorizeView.
Input components
Renames (applied by the update):
| Before | After |
|---|---|
LgSearchNavigation | LgNavigationBox (the Scrollable parameter disappears) |
LgInputRadioGroup / LgInputRadio | LgRadioGroup / LgRadio (Name → GroupName) |
OnChanging (LgCheckBox, LgToggle) | OnChangingAsync |
ChangingEventArgs | ChangingItemEventArgs |
<LgDateBox DateBoxKind="Week/Month/Year"> | LgWeekBox / LgMonthBox / LgYearBox |
LgOptionListItem TItemValue="…" | LgOptionListItem TValue="…" |
New features:
LgPasswordBox: password input field.LgWeekBoxandLgMonthBox: input for a week or month, following theLgYearBoxpattern (replaceDateBoxKind).CascadingTypeParameteronLgSelectandLgSelectMultiple: 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 adaptsindex.htmland 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'smain.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
EulaAcceptancestable). OnConfigureHttpClient(IHttpClientBuilder, string): new method in the clientMainclass to configure the application'sHttpClient.- 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.
| Constant | 3.x value | 4.0 value |
|---|---|---|
CollationProvider.SQLServer | Microsoft.EntityFrameworkCore.SqlServer | SQLServer |
CollationProvider.Oracle | Oracle.EntityFrameworkCore | Oracle |
CollationProvider.MySQL | Pomelo.EntityFrameworkCore.MySql | MySQL |
CollationProvider.PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL | PostgreSQL |
CollationProvider.SQLite | Microsoft.EntityFrameworkCore.Sqlite | SQLite |
CollationProvider.MariaDB | MariaDB | MariaDB (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
LagoonDefaultCompressiontofalsein the solution'sDirectory.Build.props: gzip variants are restored and uncompressed files kept. - The 3.x property
DisablePublishCleaningcauses a compilation error: remove it.
<Project>
<PropertyGroup>
<LagoonDefaultCompression>false</LagoonDefaultCompression>
</PropertyGroup>
</Project>
See Static file compression for format details, the exclusion list, and verification steps.