Skip to main content

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.

Applications on 4.0 beta versions

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.0 to net10.0. .NET 6 is no longer supported.

  • lgn tool 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 ef tool installed: the update generates an EF Core migration (see step 5).

  • DbContext configuration conforming to the Lagoon model, otherwise the LagoonMigrationV4 migration cannot be generated correctly:

    • the Model project contains the file Context/DesignTimeDbContextFactory.cs. dotnet ef uses it to instantiate the context, and the update rewrites it to derive from DesignTimeApplicationDbContextFactory<T>;
    • Context/ApplicationDbContext.cs exposes a static method ApplicationDbContext.Configure(DbContextOptionsBuilder, IConfiguration, bool). It holds all the context configuration: provider, connection string, and the UseOpenIddict(…) call that the update converts to UseOpenIddict<Guid>();
    • this same method is called by Main.OnConfigureDbContext on 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.cs
    protected 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.

  • 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-major allows upgrading to a new major version of Lagoon packages (3.x → 4.0). Without this option, lgn stays on the latest 3.x.
  • --dotnet-fw migrates the .NET framework: targets net10.0, packages Microsoft.* 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 (error CS0246), to be replaced with the type of the grid row (TItem).
MarkerCauseAction
LAGOON_SET_TItem_HereColumn 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 old LgGridColumn. The update converted it to LgGridTextColumn, but the bound value is not a string. Replace it with the type-specific column (LgGridNumericColumn, LgGridDateColumn, LgGridBooleanColumn, LgGridEnumColumn…) or with LgGridCustomColumn. In general, semantic analysis has already made this correction.
  • LgDynamicGridView not found: the component is removed and no automatic conversion exists. Replace it with a typed LgGridView<TItem>. Generate variable columns with a @foreach loop in <Columns>, bound by indexer on the row (@bind-Value="@item.Values[key]"), and provide a populated instance via ItemModel. See Dynamic columns.
  • TItem required by the compiler on a column without @bind-Value (a column defined by ValueSelector, for example): add TItem="…" on the column.
  • No constructor of LgEulaManager takes 3 arguments: the constructor now receives the LgHtmlSanitizer service. Add it to the code that instantiates or derives LgEulaManager. 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 a Guid key (UseOpenIddict<Guid>(), automatically rewritten in ApplicationDbContext) and move to OpenIddict 7 schema. If the key was a string, 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 AspNetUserTokens and AspNetUserLogins change from 450 to 128 characters. Verify that no existing values exceed this length.
  • New EulaAcceptances table: 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 replaces app.UseStaticFiles() with endpoints.MapStaticAssets() at the beginning of the UseEndpoints block. If a marker was placed (call with options, or pipeline without UseEndpoints), make the change manually:

    Main.cs (Server)
    app.UseBlazorFrameworkFiles();
    app.UseRouting();
    // ...
    app.UseEndpoints(endpoints =>
    {
    endpoints.MapStaticAssets();
    endpoints.MapRazorPages();
    endpoints.MapControllers();
    endpoints.MapLagoonFallback();
    });

    MapStaticAssets only serves files present in the build manifest: a file dropped in wwwroot after compilation is not served.

  • Application served over HTTP (without HTTPS): Brotli is only negotiated on a secure origin, the server would respond with 404 or an empty body. Set LagoonDefaultCompression in the solution's Directory.Build.props:

    Directory.Build.props
    <Project>
    <PropertyGroup>
    <LagoonDefaultCompression>false</LagoonDefaultCompression>
    </PropertyGroup>
    </Project>
  • The 3.x property DisablePublishCleaning now causes a compilation error: remove it (and use LagoonDefaultCompression if needed).

See Static file compression.

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 named context and 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-Value becomes a ValueSelector.
Return 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 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:

AreaChanges
Projectsnet10.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.
ServerUseStaticFiles() → endpoints.MapStaticAssets().
DatabaseUseOpenIddict<Guid>(), LagoonMigrationV4 migration adapted to the engine, DesignTimeDbContextFactory derived from DesignTimeApplicationDbContextFactory<T>.
ComponentsLgSearchNavigation → 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.
GridLgGridColumn → 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.
StylesSCSS variables $auth-app-loading-* → $app-loading-*.