Migrate from 3.x to 4.0
This page describes, in order, the steps to upgrade a Lagoon 3.x application to 4.0. The lgn update command applies most of the changes. This page focuses on what must be done manually. Details of each change can be found in the Changelog.
The 4.0 migration rules do not apply to applications already upgraded to a 4.0.x beta version: only run lgn update.
1. Prerequisites
-
SDK .NET 10 installed: applications upgrade from
net8.0tonet10.0. .NET 6 is no longer supported. -
lgntool 4.0.5 or higher. The 4.0 migration files require it: with an older tool, the update stops with an error.dotnet tool update LGN -g -
dotnet eftool installed: the update generates an EF Core migration (see step 5). -
DbContextconfiguration conforming to the Lagoon model, otherwise theLagoonMigrationV4migration cannot be generated correctly:- the
Modelproject contains the fileContext/DesignTimeDbContextFactory.cs.dotnet efuses it to instantiate the context, and the update rewrites it to derive fromDesignTimeApplicationDbContextFactory<T>; Context/ApplicationDbContext.csexposes a static methodApplicationDbContext.Configure(DbContextOptionsBuilder, IConfiguration, bool). It holds all the context configuration: provider, connection string, and theUseOpenIddict(…)call that the update converts toUseOpenIddict<Guid>();- this same method is called by
Main.OnConfigureDbContexton the server and by the design-time factory. The model used to generate the migration is thus identical to the one at runtime.
Server/Main.csprotected override void OnConfigureDbContext(DbContextOptionsBuilder builder){ApplicationDbContext.Configure(builder, Configuration, ApplicationInformation.IsDevelopment);}Model/Context/DesignTimeDbContextFactory.cs (3.x)public ApplicationDbContext CreateDbContext(string[] args){IConfiguration configuration = DesignTimeConfiguration.GetDevelopmentConfiguration();DbContextOptionsBuilder<ApplicationDbContext> builder = new();ApplicationDbContext.Configure(builder, configuration, true);return new ApplicationDbContext(builder.Options);}If your application configures the context differently (options written directly in
OnConfigureDbContext, factory missing or customized), bring it to this model before launching the update. - the
-
A clean working copy (everything committed), to review the diff produced by the update.
-
A database backup for each environment before applying the migration.
2. Run the update
From the solution folder:
lgn update --allow-major --dotnet-fw
--allow-majorallows upgrading to a new major version of Lagoon packages (3.x → 4.0). Without this option,lgnstays on the latest 3.x.--dotnet-fwmigrates the .NET framework: targetsnet10.0, packagesMicrosoft.*to 10.x,.vscode/files created or updated.
For grids, the update first performs a semantic analysis of the code: it compiles the application to determine the actual type of columns (TItem, TColumnValue).
3. Handle markers left by the update
When a change cannot be converted safely, lgn leaves a marker in the code instead of guessing. Search the entire solution for:
LAGOON UNSUPPORTED: a<-- LAGOON UNSUPPORTED: … --!comment that explains the action to take;LAGOON_SET_TItem_Here: deliberately invalid type (errorCS0246), to be replaced with the type of the grid row (TItem).
| Marker | Cause | Action |
|---|---|---|
LAGOON_SET_TItem_Here | Column or OnCellClick handler whose row type could not be inferred (multiple grids of different types in the same file, or semantic analysis disabled). | Replace with the type of the grid rows, for example TItem="OrderRow" or GridViewCellClickEventArgs<OrderRow>. |
this template captures the Columns context variable … | A CellContent / EditCellContent template uses the variable from <Columns Context="x">. In 4.0, this variable designates the row model, not the displayed row. | Read the row via the cell context: cell.Item.Xxx for the item, cell.Value for the bound value. See Grid: direct cell rendering. |
replace "InputMaskOptions" … | The column parameter InputMaskOptions is removed. | Declare InputMask, InputMaskKind, InputMaskPlaceholder, AutoUnmask, ClearIncomplete, or Placeholder directly on the column. |
bare return in a now Task-returning handler … | An OnValueChange handler became OnValueChangingAsync and now returns a Task. | Replace return; with return Task.CompletedTask; (or make the method async). |
"MapStaticAssets()" now serves the static assets … / replace "UseStaticFiles()" … | The server's Main.cs calls UseStaticFiles with options, or has no UseEndpoints block. | See step 6. |
review this generated migration … | LagoonMigrationV4 migration generated (OpenIddict keys). | See step 5. |
Replace 'Roles' with 'Policy' | [Authorize(Roles = …)]: role-based authorization is not supported (rule already present in 3.x). | Use a policy. |
4. Compile and fix remaining errors
Compile the solution. Expected errors are few and well-identified:
cannot convert 'X' to 'string'on an oldLgGridColumn. The update converted it toLgGridTextColumn, but the bound value is not a string. Replace it with the type-specific column (LgGridNumericColumn,LgGridDateColumn,LgGridBooleanColumn,LgGridEnumColumn…) or withLgGridCustomColumn. In general, semantic analysis has already made this correction.LgDynamicGridViewnot found: the component is removed and no automatic conversion exists. Replace it with a typedLgGridView<TItem>. Generate variable columns with a@foreachloop in<Columns>, bound by indexer on the row (@bind-Value="@item.Values[key]"), and provide a populated instance viaItemModel. See Dynamic columns.TItemrequired by the compiler on a column without@bind-Value(a column defined byValueSelector, for example): addTItem="…"on the column.- No constructor of
LgEulaManagertakes 3 arguments: the constructor now receives theLgHtmlSanitizerservice. Add it to the code that instantiates or derivesLgEulaManager. See Terms of use.
5. Database
The update generates the EF Core migration LagoonMigrationV4 in the Model project. Review it before applying (it bears the marker review this generated migration), then remove the marker.
What this migration does:
- OpenIddict keys in
Guid: OpenIddict entities now always use aGuidkey (UseOpenIddict<Guid>(), automatically rewritten inApplicationDbContext) and move to OpenIddict 7 schema. If the key was astring, the 4 OpenIddict tables are emptied before conversion. Consequence: all users will need to log in again. OpenIddict applications and scopes are re-seeded on startup. - ASP.NET Identity keys: key columns of
AspNetUserTokensandAspNetUserLoginschange from 450 to 128 characters. Verify that no existing values exceed this length. - New
EulaAcceptancestable: records the date when terms of use were accepted. - On SQL Server, MySQL/MariaDB and PostgreSQL, the migration frees and recreates keys and indexes carried by the converted columns, as these engines require.
Then apply the migration as usual (see Database update), first on a test environment, with a backup.
For MySQL / MariaDB, the Pomelo provider is replaced by its fork Microting.EntityFrameworkCore.MySql (automatic). Existing migrations remain valid.
Application code that compares Database.ProviderName to a CollationProvider constant should pass through DatabaseEngine.FromProviderName(…): the constants now contain an engine name (SQLServer, MySQL…) rather than an assembly name.
6. Publishing and static files
Publishing now produces only Brotli variants (.br) and deletes the original of each compressed file. Only MapStaticAssets knows how to serve these files.
-
Server's
Main.cs: the update replacesapp.UseStaticFiles()withendpoints.MapStaticAssets()at the beginning of theUseEndpointsblock. If a marker was placed (call with options, or pipeline withoutUseEndpoints), make the change manually:Main.cs (Server)app.UseBlazorFrameworkFiles();app.UseRouting();// ...app.UseEndpoints(endpoints =>{endpoints.MapStaticAssets();endpoints.MapRazorPages();endpoints.MapControllers();endpoints.MapLagoonFallback();});MapStaticAssetsonly serves files present in the build manifest: a file dropped inwwwrootafter compilation is not served. -
Application served over HTTP (without HTTPS): Brotli is only negotiated on a secure origin, the server would respond with
404or an empty body. SetLagoonDefaultCompressionin the solution'sDirectory.Build.props:Directory.Build.props<Project><PropertyGroup><LagoonDefaultCompression>false</LagoonDefaultCompression></PropertyGroup></Project> -
The 3.x property
DisablePublishCleaningnow causes a compilation error: remove it (and useLagoonDefaultCompressionif needed).
7. Build and continuous integration
The lgn tool is no longer required to build a Lagoon application: resources (CSS, JS, icons, dictionaries, index pages) are generated by the Lagoon.Build package's MSBuild tasks. A developer machine or build agent only needs the .NET SDK. lgn remains useful for its other commands (new, update…).
8. Check for silent behavior changes
Some changes compile without error but modify rendering. Check your grids:
Grid: direct cell rendering
The grid now executes the <Columns> fragment only once, on a row model, then renders each cell with compiled accessors. A column parameter that read the context variable (DisplayFormat="@(item.IsLate ? "N0" : "N2")", HideContent="@item.Masked"…) then gives the same value on all rows.
- The update converts simple cases to per-row selectors (
HideContentSelector,DisplayFormatSelector,PrefixSelector,CommandNameSelector…), but only when the context is namedcontextand the tag fits on one line. Review the others manually: look for column parameters that use the variable from<Columns Context="…">. - A
Value="@(context…)"without@bind-Valuebecomes aValueSelector.
There is no global option: the grid automatically reverts to the old rendering, executing <Columns> for each row, as soon as a visible column carries a value without an expression. This occurs when the value is passed by Value="…" alone, without @bind-Value or ValueSelector. This mode remains available for a template that truly needs to capture the row, but it loses the performance gain of direct rendering. Such a column is neither sortable nor filterable, unless you give it a SortSelector / FilterSelector.
Terms of use in .htm format
The server now sanitizes the content of terms of use (dynamic mode and shared mode). In a .htm file exported from a word processor, style elements and the class attribute are preserved, but meta, title, script, and forms are removed. Check the display of your terms of use. See Server-side HTML sanitization.
Removed column parameters
The filter parameters (AllowFilter, DefaultFilter, FilterExpression…), MaxLength and InputDisplayFormat are no longer declared on columns that don't use them. The update removes them. Warning: Razor does not flag an unknown attribute. Check attributes added manually after migration.
What the update does automatically
For reference, lgn update --allow-major --dotnet-fw notably applies:
| Area | Changes |
|---|---|
| Projects | net10.0, packages Microsoft.AspNetCore.* / *EntityFrameworkCore* to 10.x, Pomelo → Microting, EF Core versions pinned in Model and Services, SQLitePCLRaw.bundle_e_sqlite3 (CVE-2025-6965), AsyncFixer 2, removal of obsolete packages (Microsoft.AspNetCore.Http, Identity.UI, Mvc.NewtonsoftJson, System.Text.Json). |
| Tooling | .vscode/ files (launch.json, tasks.json, settings.json, extensions.json) created and shared via .gitignore; generated_code block in .editorconfig. |
| Server | UseStaticFiles() → endpoints.MapStaticAssets(). |
| Database | UseOpenIddict<Guid>(), LagoonMigrationV4 migration adapted to the engine, DesignTimeDbContextFactory derived from DesignTimeApplicationDbContextFactory<T>. |
| Components | LgSearchNavigation → LgNavigationBox (without Scrollable), LgInputRadioGroup / LgInputRadio → LgRadioGroup / LgRadio (Name → GroupName), OnChanging → OnChangingAsync (LgCheckBox, LgToggle), LgDateBox + DateBoxKind → LgWeekBox / LgMonthBox / LgYearBox, ChangingEventArgs → ChangingItemEventArgs, type parameter TValue on LgOptionListItem, mask parameters removed from LgSummaryBox. |
| Grid | LgGridColumn → LgGridTextColumn / LgGridCustomColumn, CellContent context renamed cell, OnValueChange → OnValueChangingAsync (handler signature included), OnCellClick typed GridViewCellClickEventArgs<TItem>, *Expression → *Selector, per-row parameters → *Selector, JsResizeColumnsAsync / RefreshCssVariablesAsync → AutoResizeColumnsAsync, Confirmation → ConfirmationMessage on columns, orphaned parameters removed. |
| Styles | SCSS variables $auth-app-loading-* → $app-loading-*. |