Crossing Fluent UI Blazor 4 → 5
The portal's Blazor front end renders the framework's controls through Fluent UI Blazor
(Microsoft.FluentUI.AspNetCore.Components and .Icons, pinned in
src/Directory.Packages.Plugins.props — core carries no GUI technology, policy
core-carries-no-gui-technology). Version 5 is not an upgrade of version 4: it is a rewrite on the
Fluent 2 web components, with its own component names, enums, dialog and theme systems and CSS
tokens. This page is the record of the crossing, in the shape of core's
Dependency Major Upgrades ledger, and the reference for
what the platform now carries itself.
It is also a step in the direction The design system sets: away from Fluent's look,
onto the platform's --mw-* tokens. Nothing here reverses that direction; where v5 removed a
component the portal relied on, the replacement is the documented v5 one, or a platform piece that
already existed.
Ledger entry
Microsoft.FluentUI.AspNetCore.Components 4.14.4 → 5.0.1 (and .Icons 4.14.4 → 5.0.1)
| Step | Result |
|---|---|
| Registry | …/v3-flatcontainer/microsoft.fluentui.aspnetcore.components/index.json: newest stable 5.0.1 (after 5.0.0 and five release candidates); .icons: newest stable 5.0.1. Both .nupkgs downloaded and extracted. |
| Licence | MIT at both ends — <license type="expression">MIT</license> in the 4.14.4 and 5.0.1 nuspecs of both packages. No change. |
| Target frameworks | 5.0.1 ships net8.0, net9.0, net10.0, net11.0. The crossing rides the platform-4 line (net11.0, MeshWeaver.Plugins#5421), so the portal binds the net11.0 asset; nothing in v5 itself required net11 — the move was first built and tested on net10.0 as well. |
| Dependency floor | The net11.0 group requires Microsoft.AspNetCore.Components.Web >= 11.0.0-rc.1 (the net10.0 group >= 10.0.12). The platform-4 line already pins Components.Web at 11.0.0, so no pin moves. |
| Public API | MetadataLoadContext over the net10.0 assets: 603 → 539 public types; 400 type names removed (generic-arity renames included — FluentDatePicker → FluentDatePicker<TValue>, the list components' second type parameter), 336 added, 203 kept; 4,140 public member lines removed. 61 removed type names were referenced by this repository's src/ (by name; the count includes common words such as Option that also name non-Fluent types). |
| Library self-check | No EnablePackageValidation baseline and no CompatibilitySuppressions in 5.0.1 — the maintainers ship a migration guide (Documentation/GetStarted/Migration/*.md, 60 pages) instead, which this crossing followed component by component. No analyzers ship in either package. |
| Obsolete members | v5 keeps the v4 enums that the guide maps (Appearance, FluentInputAppearance, TooltipPosition, …) in a Migration folder with every member [Obsolete], so -warnaserror (CS0618) refuses each use. None is used any more; the conversions live in platform code that names v4's WIRE names (below). |
| CSS tokens | v4's FluentDesignTheme computed --neutral-*, --accent-*, --type-ramp-*, … in JavaScript; v5 writes the Fluent 2 tokens (--colorNeutralForeground1, --colorBrandBackground, …) instead. Measured across this repository and core: 1,790 reads of 61 distinct v4 names. Core's DesignTokens.FluentAliases already defines 62 of them; 32 names (105 reads) were defined by nothing on v5 — see The CSS tokens below. |
| In-mesh source | No NodeType source, script or test in a module folder references a Fluent type (git grep Microsoft.FluentUI outside src/: none). In-mesh layout areas reach the renderer through the UiControl records, whose wire vocabulary (appearance names, typographies) the views now translate explicitly. Their CSS strings read v4 token NAMES — which is why those names stay defined. |
| Behaviour the compiler did not see | The suites found four, each a runtime throw or a markup change behind a compiling call: every v5 input REFUSES a null Id (FluentInputBase.SetParametersAsync throws InvalidOperationException, where v4 minted its own — FormComponentBase.InputId now mints one); FluentRadio must carry its group's TValue (an untyped radio throws); every v5 input is wrapped in a <fluent-field> (label + input + message), which is now the box a dialog's full-width rule must reach; and the date picker renders its calendar into the markup, day buttons and their aria-labels included. The accessible name goes through the inputs' own AriaLabel parameter, which each v5 input routes to its own element. |
| Builds | Every project that references Fluent or a Blazor project built -c Release -warnaserror, 0 Error(s) — against core main (net10.0) and again against the platform-4 tree (net11.0), both portal hosts included. |
| Suites | 9 GUI suites on net11.0, 3,120 tests, 0 failed — see Suites below. |
The mappings applied
The migration guide's tables, applied as written; the left column is v4, the right what the portal renders now.
| v4 | v5 | Where |
|---|---|---|
FluentDesignTheme StorageName="theme" (mode + Office accent) |
ThemeSync → ThemeStore (platform): the same "theme" key and JSON shape, plus IThemeService.SetThemeAsync for v5's own tokens |
App, pages, top bar, public layout, Settings → Appearance |
DesignThemeModes (System = 0, Light, Dark) |
ThemeMode (Light = 0, Dark, System) — JS callers now receive the NAME, never the ordinal |
cascaded ThemeMode, Monaco, Mermaid, Markdown |
OfficeColor / OfficeColorUtilities |
AccentPresets (platform; v4's 25 names and hex values unchanged) |
Appearance picker, design-token stylesheet |
FluentDialogProvider, FluentToastProvider, FluentTooltipProvider, FluentKeyCodeProvider, FluentMenuProvider |
FluentProviders (menus need no provider; toast count and lifetime are library configuration) |
AreaPage, PortalLayoutBase, PublicLayout |
AddFluentUIComponents() |
AddFluentUIComponents(FluentLibraryDefaults.Apply) — restores v4's 10px FluentStack gaps and FluentGrid Spacing=3, sets 3 toasts / 5 s |
hosting and every bUnit test |
FluentButton Appearance=Appearance.* |
ButtonAppearance: Accent → Primary, Neutral/Hypertext/Filled → Default, Lightweight → Transparent, Outline → Outline, Stealth → Subtle (MwAppearanceExtensions.ToButtonAppearance for wire names) |
every button, MenuItemView |
FluentAnchor (button-styled) |
FluentAnchorButton |
logo, header links, sign-in, not-found page |
FluentAnchor (hypertext) |
FluentLink; Target → LinkTarget; rel="noopener noreferrer" → implicit noopener on _blank + ReferrerPolicy=NoReferrer (LinkTargets) — FluentLink has no free rel |
LinkView, file-browser crumbs |
FluentBreadcrumb / FluentBreadcrumbItem |
FluentStack of FluentLinks with chevron icons (the guide's documented replacement) |
FileBrowser |
FluentToolbar |
FluentStack (horizontal, wrapping, 8px gap) |
ToolbarView, FileBrowser |
FluentHeader / FluentFooter / FluentBodyContent |
FluentLayoutItem Area=Header/Footer/Content |
skins, portal header |
FluentLabel Typo=… |
FluentText As=… Size=… (LabelTypography: v4's tag and type-ramp step per typography) |
Label view, every page label |
FluentTextField / FluentSearch |
FluentTextInput (search icon in StartTemplate; Size in characters → Width in ch) |
text fields, mobile menu search |
FluentNumberField |
FluentNumberInput |
dialogs, tests |
FluentTextArea Rows/Cols |
Height (rows × 20px + 12px) / Width (ch) |
TextAreaView |
FluentProgressRing |
FluentSpinner (style widths → SpinnerSize) |
26 sites |
FluentProgress Stroke/Paused |
FluentProgressBar Thickness (Paused gone: a Value means still) |
ProgressView, file upload |
FluentSelect/Combobox/Listbox (SelectedOption) |
two type parameters, @bind-Value, OptionValueToString |
list views, editors, onboarding |
FluentDatePicker |
FluentDatePicker<DateTime?>, CalendarDayFormat |
DateTimeView |
FluentPersona |
FluentAvatar |
profile, image upload |
FluentProfileMenu |
FluentButton + FluentPopover + FluentAvatar (the guide's documented replacement) |
UserProfile |
FluentCounterBadge wrapping the bell |
FluentCounterBadge with AnchorContent, OverflowCount=99 |
NotificationCenter |
FluentBadge Appearance/Color(string)/Circular/Fill/Dismiss* |
BadgeAppearance + BadgeColor (BadgeLooks), Shape; CSS colour/size as style |
BadgeView, catalog |
FluentMessageBar Intent=MessageIntent.* |
MessageBarIntent.* |
message bars |
FluentMenu Anchor/Open/OpenChanged |
FluentMenu Trigger= + FluentMenuList; open state owned by the web component |
top-bar menus |
FluentMenu Open Anchored=false (inline panel) |
standalone FluentMenuList, rendered while open |
mobile menu |
FluentDialog + FluentDialogHeader/Footer (inline) |
FluentDialog + FluentDialogBody TitleTemplate/ActionTemplate, opened with ShowAsync |
DialogView, camera sheet |
IDialogService.ShowDialogAsync<T,TData>(data, DialogParameters) + IDialogContentComponent<T> |
ShowDialogAsync<T>(options ⇒ …Parameters…), cascaded IDialogInstance |
file dialogs, node preview |
ShowPanelAsync |
ShowDrawerAsync (DialogAlignment.End) |
notification centre |
ShowMessageBoxAsync(MessageBoxContent) |
ShowConfirmationAsync / ShowErrorAsync (message is plain text the dialog encodes) |
recycle, projection, error modal |
IToastService.ShowSuccess/Error/Warning |
INotificationService.Show…ToastAsync through ToastNotifications (the v5 task completes when the toast CLOSES, so it is observed, never awaited) |
file browser, edit form |
FluentAccordionItem HeadingTemplate |
HeaderTemplate |
expander, catalog |
FluentGridItem xs/sm/… |
Xs/Sm/… |
catalog page |
GenerateHeaderOption, Align, SortDirection |
DataGridGeneratedHeaderType, DataGridCellAlignment, DataGridSortDirection |
DataGridView |
FluentSwitch Checked/UncheckedMessage |
removed (no v5 equivalent) | switch views |
FluentRadio child content |
Label |
radio groups |
Id="@InputId" with a null id (v4 minted one) |
InputId is never null — the view mints mw-input-… once |
every form view |
raw aria-label on inputs |
the inputs' AriaLabel parameter |
every form view |
FluentRadio Value=@object |
FluentRadio TValue="TValue" |
RadioGroupView |
JSModule (v4 public base class) |
the accessor holds its own module | CacheStorageAccessor |
fluent-text-field, fluent-search, fluent-number-field, fluent-text-area, fluent-progress-ring, fluent-anchor, fluent-select/fluent-combobox (CSS/JS tag selectors) |
fluent-text-input, fluent-textarea, fluent-spinner, fluent-anchor-button, fluent-dropdown; [appearance=stealth/accent/lightweight] → subtle/primary/transparent |
stylesheets, ButtonView.razor.js, thumbnail generator |
SiteSettingsPanel (a v4 demo panel nothing referenced) was deleted rather than ported; Settings →
Appearance (AppearanceView) is the live surface for the same choices.
What the platform now carries itself
- The theme store (
MeshWeaver.Blazor/Components/ThemeStore.razor+ThemeSync.razor.js). It keeps v4's storage key and shape, so a choice made before the upgrade survives it, and the pre-paint script inApp.razorandthemeHandlerare unchanged. Every store on the page hears the others (one window event), so the top-bar switch re-themes the editors and diagrams that read the cascadedThemeMode.ThemeSyncwraps it inChromeErrorBoundary, exactly as it wrappedFluentDesignTheme(MeshWeaver.Plugins#1868, MeshWeaver#1108) —ChromeErrorBoundaryTeststill proves both negative controls and the containment. - The accent presets (
AccentPresets), v4'sOfficeColortable, which the design-token stylesheet and the pre-paint script resolve stored names through. - The wire translations —
MwAppearanceExtensions.ToButtonAppearance,BadgeLooks,InputAppearances,LabelTypography,LinkTargets. A control model's appearance or typography travels as a NAME (v4's member names); the views translate it explicitly, so a model authored before the upgrade renders the same.
The CSS tokens
Three blocks keep every rendered colour on the platform tokens (/_mw/design-tokens.css,
Memex.Portal.Gui/Theme):
- core's
DesignTokens.FluentAliases— v4's colour names onto--mw-*(unchanged); FluentV5TokenAliases— v5's Fluent 2 names onto--mw-*, applied like core's block (!important, on the root and on every element with an inline custom property), so v5's own theme script cannot win over the visitor's accent;FluentV4TokenRemainder— the 32 v4 names nothing else defines any more: shape and type-ramp tokens at v4's own default values, the remaining colours on--mw-*. They are DEFINED rather than rewritten because in-mesh C# and authored content read them and no compiler sees those reads.
Suites
| Suite | net10.0, first run | net10.0, final | net11.0 (platform-4 line) |
|---|---|---|---|
MeshWeaver.Blazor.Views.Test |
868 total, 34 failed | 868, 0 failed | 877, 0 failed |
MeshWeaver.Blazor.EntityViews.Test |
88 total, 30 failed | 88, 0 failed | 88, 0 failed |
Memex.Portal.Gui.Test |
1,072, 0 failed | 1,072, 0 failed | 1,072, 0 failed |
MeshWeaver.Blazor.Chat.Test |
155, 0 failed | 155, 0 failed | 155, 0 failed |
MeshWeaver.Blazor.Portal.Test |
216, 0 failed | 216, 0 failed | 216, 0 failed |
MeshWeaver.Graph.Views.Test |
369, 0 failed | — | 369, 0 failed |
MeshWeaver.Hosting.Blazor.Test |
276, 0 failed | — | 276, 0 failed |
MeshWeaver.Blazor.Graph.Test |
41, 0 failed | — | 41, 0 failed |
MeshWeaver.Blazor.Analysis.Test |
26, 0 failed | — | 26, 0 failed |
Run as the xUnit v3 executables (bin/Release/<tfm>/<Project>), each built -c Release -warnaserror.
The 64 first-pass failures were the crossing's real findings, not flakes: most were the null-Id
refusal; then the untyped FluentRadio, the <fluent-field> wrapper the dialog full-width rule missed,
a module-level listener registry in the new theme JS (ComponentJsIsInstanceScopedGuard), and a static
renderer without v5's LibraryConfiguration / IJSRuntime; the rest asserted v4 markup (<fluent-anchor,
rel="noopener noreferrer", the message bar's intent-* class, fluent-toolbar, the card's class,
[appearance="accent"]) and now assert the v5 markup that carries the same property.
CI: the platform image still ships Fluent 4
A local build restores v5 and is green; CI is not, because the shared lane compiles the repository
INSIDE the pinned platform image (build-project --bind-to-image, image memex-portal-ai), and that
image ships Microsoft.FluentUI.AspNetCore.Components 4.14.4. The global workspace build fails
MeshWeaver.Blazor with RZ10012/RZ9996 (FluentSpinner, FluentText, TitleTemplate, …) —
v5 names compiled against the v4 assembly.
The lane's superseded-image-assemblies input is not a bridge for this as it stands, measured
against core's lane and tester (node-repo-module-pack.yml, tools/MeshWeaver.PluginTester):
- It is refused as stale before anything compiles.
SupersededEntryStaleness(MeshWeaver#3223) refuses an entry when the image's copy defines none of the fully-qualified type names this repository's source declares. The input exists for a type that MOVED out of an image assembly into this repository's source; no file here declares aMicrosoft.FluentUI.AspNetCore.Components.*type, so the overlap is empty and the tester throws--superseded-image-assembly '…': STALE. - Nothing would supply v5 in its place. The container build has no NuGet restore: beyond the
image it binds only the module-libraries shelf (
src/MeshWeaver.ModuleLibraries), which carries no Fluent package. Dropping v4 from the reference set alone would turn every Fluent use intoCS0246.
So the crossing lands when the image the lane binds carries Fluent 5 — or the lane grows a declared, version-scoped "this repository rides a newer package than the image" entry (with the package on the shelf), which is a change to core's lane and tester, not to this repository.
What was not verified
No browser was driven. Every visual statement on this page is a statement about markup and CSS, not about pixels. In particular, unverified in a running portal:
- the Fluent 2 web components' look against the
--mw-*aliases (radii, shadows, focus rings), in light and dark, for each accent; - the
::part(…)overrides instandard-page-layout.css,PortalLayoutBase.razor.cssand the views' scoped CSS: they were written against v4's shadow parts, and v5's web components expose different ones, so some are now inert; FluentLayoutItemas the portal header (it wasFluentHeader), the profile popover's placement, the drawer the notification centre opens, the top-bar menus opening from their triggers, and the inline mobile menu;- icon colour: v4 drew an uncoloured
FluentIconin the accent, v5 incurrentColor; - the Playwright suite (
src/MeshWeaver.Portal.E2E.Test,e2e/), whose selectors name v4 element tags in places.