You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
.AsFileUpload(acceptedFileTypes: [".pdf"], maxFileSize: 5 * 1024 * 1024) — the public, documented way
to constrain a file upload — is silently ignored on the single-file MudBlazor path. No accept
filter, no size cap, no exception, no diagnostic. A 500 MB .exe is accepted by a field the developer
constrained to 5 MB of PDF.
Verified on dev (01a6580), reading the two halves against each other:
FormCraft/Forms/Extensions/FieldBuilderExtensions.cs:299 — AsFileUpload writes exactly one
attribute, and nothing else:
varconfig=newFileUploadConfiguration{AcceptedFileTypes= …,MaxFileSize= …,MaxFiles=1, … };// No renderer override: the field type dispatches to the UI framework's// file upload component, which reads this configuration attribute.builder.WithAttribute("FileUploadConfiguration",config);
FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs:38-44 — OnInitialized reads only raw keys, and never FileUploadConfiguration:
Every one of those comes back null. The comment in AsFileUpload is therefore false for this
component: it asserts the component reads the configuration attribute, and it does not.
The sibling gets it right — MudBlazorMultipleFileUploadComponent.razor.cs:36-37 does GetAttribute<FileUploadConfiguration>("FileUploadConfiguration") — so the two MudBlazor upload
components disagree about where constraints come from, and only one of them matches the builder.
Reported, same root, needs confirming:FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs
(~line 111) resolves its fallback as GetAttribute("MaxFileSize", GetAttribute("MaximumFileSize", 10L * 1024 * 1024)),
which infers T = long. FieldComponentBase.GetAttribute returns the stored value only when value is T, so a .WithAttribute("MaxFileSize", 5_000_000) — which boxes an int — fails is long
and silently reverts to the 10 MB default.
Root cause. There is no single path by which an upload component resolves its constraints. Four
components each pick their own keys and their own CLR types, so each drops a different subset,
silently. The repository already treats this failure mode as an architecture invariant — see .claude/skills/repo-profile.md → Architecture grain, ⛔ There is ONE render path, written after #146/#177/#184/#190 each arrived as a separate bug report for one capability implemented twice. This is
the same shape, in the upload components.
Why it deserves priority:medium. A dropped maxFileSize/acceptedFileTypes is security-adjacent:
the developer wrote a constraint, the UI enforces nothing, and nothing anywhere reports the gap.
Proposed solution
Give upload constraints one resolution path, used by every upload component:
Resolve FileUploadConfiguration first, falling back to the raw per-key attributes for
hand-written .WithAttribute(...) callers (that fallback is why the raw keys exist).
Make the fallback type-tolerant for numerics, so int and long both bind — the Fluent MaxFileSize bug is exactly this coercion missing.
Have all four components (MudBlazor single + multiple, Fluent single + multiple) consume that one
path, so a new constraint is added once rather than four times.
Cover it with a parity test asserting the same .AsFileUpload(...) configuration reaches every
upload component — the drift is what the guard has to catch, not any single component's reading.
Alternatives considered
Just add FileUploadConfiguration to the single-file MudBlazor component. One line, fixes the
worst symptom today. Rejected as the whole fix: it leaves four independent readers and the Fluent int/long bug intact, so the next constraint drifts again — this issue exists precisely because
the per-component approach already failed once.
Make AsFileUpload also write the raw keys. Tempting (no component changes at all). Rejected:
it doubles every attribute at the source, so the two spellings can disagree, and any consumer
reading only one of them is still wrong — it moves the drift rather than removing it.
Drop the raw-key fallback and require FileUploadConfiguration. Cleanest model. Rejected as a
breaking change to hand-written .WithAttribute("Accept", …) callers, which is out of proportion to
a bug fix; the fallback can stay as long as one path owns it.
Area
UI adapters — file upload field components (FormCraft.ForMudBlazor, FormCraft.ForFluentUI) and FieldBuilderExtensions.AsFileUpload
FormCraft's core is UI-framework-agnostic and dispatches every field through IFieldRendererService to
a per-framework component. Constraints travel as attributes on the field configuration, which makes
the contract between builder and component implicit: the builder writes a key, the component reads a
key, and nothing checks the two agree. When they disagree the failure is total and silent — the
constraint simply does not exist at runtime.
That is what happened here: AsFileUpload was changed (or written) to publish a single FileUploadConfiguration object, the multiple-file component was updated to read it, and the
single-file component was not. Its code still reads the older raw keys, so it sees nothing.
Approaches
A. Patch the one component. Add a FileUploadConfiguration read to MudBlazorFileUploadFieldComponent. Pros: smallest possible diff, fixes today's worst symptom. Cons: three other readers remain, each free to drift; the Fluent int/long coercion bug is
untouched; nothing prevents the fifth upload component from repeating it.
B. One shared resolution path + a parity test. Extract the resolve-config-then-fall-back-to-raw-keys
logic once, with numeric coercion, and have all four components use it. Pros: removes the class of
bug rather than an instance, and matches the invariant the profile already states for the render path;
the parity test makes a future divergence fail in CI. Cons: touches both adapters, so it is a larger
review than A.
C. Change the builder to write both spellings.Pros: no component changes. Cons: two sources of
truth that can disagree; a component reading only the raw keys still misses anything the config-only
path adds later. Moves the problem.
Recommendation
B. A is what the repo already did for presentation attributes, one at a time, across #146, #177, #184 and #190 — the profile's Architecture grain records that history and the conclusion drawn from
it. The same conclusion applies here: the unit of work is the shared path, not the symptom. The parity
test is what makes it stick, because the failure is invisible at runtime.
📋 Spec
Goal
A constraint written with .AsFileUpload(...) (or a raw .WithAttribute(...)) reaches every upload
component identically, and a future divergence fails a test rather than shipping silently.
Scope
One shared constraint-resolution helper for upload components, honouring FileUploadConfiguration
first and raw per-key attributes as fallback, with int/long tolerance for size values.
MudBlazorFileUploadFieldComponent, MudBlazorMultipleFileUploadComponent, FluentUIFileUploadComponentBase (and its two components) consume it.
A parity test asserting one .AsFileUpload(...) configuration produces the same resolved
constraints on every upload component.
Non-goals
Changing the AsFileUpload public signature, or removing the raw-key fallback (breaking).
Enforcing the constraints in new places (e.g. server-side validation) — this is about the configured
values reaching the component, not about what the component then does with them.
flowchart TD
A[".AsFileUpload(types, maxSize)"] --> B["WithAttribute('FileUploadConfiguration', config)"]
B --> C{"component reads what?"}
C -->|"MudBlazor multiple: FileUploadConfiguration"| D["constraints applied ✅"]
C -->|"MudBlazor single: raw keys only"| E["all null → NO limits ❌"]
C -->|"Fluent: raw key typed long"| F["int attribute dropped → 10MB default ❌"]
G["one shared resolver:<br/>config → raw keys → coerce numerics"] --> D
Loading
Key files
FormCraft/Forms/Extensions/FieldBuilderExtensions.cs — AsFileUpload (line ~299); its comment
about the component reading the attribute must become true, or be corrected.
FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadComponentBase.cs — the existing
shared base for upload components; the natural home for the resolver.
.AsFileUpload(acceptedFileTypes: [".pdf"], maxFileSize: N) yields a non-null accept filter and MaxFileSize == N on every upload component.
.WithAttribute("MaxFileSize", 5_000_000) (an int) binds, and does not fall through to a default.
A raw-key-only configuration still binds (the fallback is preserved).
Edge cases
int vs long is the coercion that must be handled; a boxed int fails is long.
MaxFiles = 1 is set by AsFileUpload for the single-file path — resolving the config must not
let a multiple-file component inherit a stray MaxFiles = 1.
Assert the resolved values the component ends up with, not merely that some attribute exists;
the whole defect is a value silently not arriving.
Assumptions
The raw per-key attributes remain supported for hand-written callers. If the owner would rather make FileUploadConfiguration the only spelling, that is a deliberate breaking change and a different issue.
🛠️ Implementation plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: one constraint-resolution path for upload components, so a configured limit cannot silently fail to arrive.
Architecture: UI adapters only. Core's AsFileUpload keeps its signature; the fix is in how components resolve what it writes.
Base branch dev; commit as Philippe Matray <phmatray@gmail.com>; conventional commits.
⛔ FormCraft core stays UI-framework-agnostic — no MudBlazor/Fluent type may leak into it.
⛔ Do not add a second render path — the resolver belongs in the shared upload base, applied to both placements by construction.
TreatWarningsAsErrors=true is deliberate — do not relax it.
dotnet test --filter is inert here (MTP). Use dotnet test <csproj> -c Release -- --filter-class <FQN>, or the built host directly; see .claude/skills/repo-profile.md → Build & test.
Interfaces: none — bUnit render assertions over MudBlazorFileUploadFieldComponent.
Step 1: Write a failing test: build a field with .AsFileUpload(acceptedFileTypes: [".pdf"], maxFileSize: 5 * 1024 * 1024), render the single-file MudBlazor component, and assert the resolved accept filter and max size are the configured ones.
Step 2: Run it → FAIL (both null today). Record the output.
Step 3: Add the mirror assertion for the multiple-file component and confirm it already passes — that contrast is the bug, and it is what the parity test in Task 3 generalises.
Step 4: Commit: test(mudblazor): pin that AsFileUpload constraints reach the single-file upload.
Task 2: One resolver, consumed by every upload component
Files: modify FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadComponentBase.cs (add the resolver), MudBlazorFileUploadFieldComponent.razor.cs, MudBlazorMultipleFileUploadComponent.razor.cs, FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs.
Interfaces: a protected resolver on the shared upload base returning the effective Accept / MaxFileSize / MaxFiles / ShowPreview / EnableDragDrop, resolving FileUploadConfiguration first and raw attributes second.
Step 1: Implement the resolver on the shared upload base: read FileUploadConfiguration, fall back to the raw keys, and coerce numeric sizes so a boxed int binds where a long is expected.
Step 2: Point MudBlazorFileUploadFieldComponent.OnInitialized at the resolver instead of its raw-key reads.
Step 3: Point MudBlazorMultipleFileUploadComponent at the same resolver, preserving its current behaviour (it is the one that works today — it must not regress).
Step 4: Point FluentUIFileUploadComponentBase at it, removing the 10L-typed fallback that drops int values.
Step 5: Re-run Task 1's tests → PASS.
Step 6: Correct or delete the now-misleading comment in FieldBuilderExtensions.AsFileUpload claiming the component reads the configuration attribute.
Step 7: Commit: fix(upload): resolve constraints through one path in every upload component.
Task 3: Parity guard + full verification
Files: extend the test file from Task 1 (and the Fluent equivalent under FormCraft.ForFluentUI.UnitTests).
Interfaces: none.
Step 1: Write a parity test asserting one .AsFileUpload(...) configuration resolves to the same constraints on every upload component — the drift, not any single reading, is what must fail.
Step 2: Add a case for .WithAttribute("MaxFileSize", 5_000_000) (an int) binding rather than falling back to a default.
Step 3: Run both adapters' suites via the per-class filter → green.
Step 4: Run ./build.sh Test (CI gate) → all suites green under TreatWarningsAsErrors.
Step 5: Commit: test(upload): assert every upload component resolves the same constraints.
Problem / motivation
.AsFileUpload(acceptedFileTypes: [".pdf"], maxFileSize: 5 * 1024 * 1024)— the public, documented wayto constrain a file upload — is silently ignored on the single-file MudBlazor path. No accept
filter, no size cap, no exception, no diagnostic. A 500 MB
.exeis accepted by a field the developerconstrained to 5 MB of PDF.
Verified on
dev(01a6580), reading the two halves against each other:FormCraft/Forms/Extensions/FieldBuilderExtensions.cs:299—AsFileUploadwrites exactly oneattribute, and nothing else:
FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs:38-44—OnInitializedreads only raw keys, and neverFileUploadConfiguration:Every one of those comes back
null. The comment inAsFileUploadis therefore false for thiscomponent: it asserts the component reads the configuration attribute, and it does not.
The sibling gets it right —
MudBlazorMultipleFileUploadComponent.razor.cs:36-37doesGetAttribute<FileUploadConfiguration>("FileUploadConfiguration")— so the two MudBlazor uploadcomponents disagree about where constraints come from, and only one of them matches the builder.
Reported, same root, needs confirming:
FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs(~line 111) resolves its fallback as
GetAttribute("MaxFileSize", GetAttribute("MaximumFileSize", 10L * 1024 * 1024)),which infers
T = long.FieldComponentBase.GetAttributereturns the stored value only whenvalue is T, so a.WithAttribute("MaxFileSize", 5_000_000)— which boxes anint— failsis longand silently reverts to the 10 MB default.
Root cause. There is no single path by which an upload component resolves its constraints. Four
components each pick their own keys and their own CLR types, so each drops a different subset,
silently. The repository already treats this failure mode as an architecture invariant — see
.claude/skills/repo-profile.md→ Architecture grain, ⛔ There is ONE render path, written after#146/#177/#184/#190 each arrived as a separate bug report for one capability implemented twice. This is
the same shape, in the upload components.
Why it deserves
priority:medium. A droppedmaxFileSize/acceptedFileTypesis security-adjacent:the developer wrote a constraint, the UI enforces nothing, and nothing anywhere reports the gap.
Proposed solution
Give upload constraints one resolution path, used by every upload component:
FileUploadConfigurationfirst, falling back to the raw per-key attributes forhand-written
.WithAttribute(...)callers (that fallback is why the raw keys exist).intandlongboth bind — the FluentMaxFileSizebug is exactly this coercion missing.path, so a new constraint is added once rather than four times.
.AsFileUpload(...)configuration reaches everyupload component — the drift is what the guard has to catch, not any single component's reading.
Alternatives considered
FileUploadConfigurationto the single-file MudBlazor component. One line, fixes theworst symptom today. Rejected as the whole fix: it leaves four independent readers and the Fluent
int/longbug intact, so the next constraint drifts again — this issue exists precisely becausethe per-component approach already failed once.
AsFileUploadalso write the raw keys. Tempting (no component changes at all). Rejected:it doubles every attribute at the source, so the two spellings can disagree, and any consumer
reading only one of them is still wrong — it moves the drift rather than removing it.
FileUploadConfiguration. Cleanest model. Rejected as abreaking change to hand-written
.WithAttribute("Accept", …)callers, which is out of proportion toa bug fix; the fallback can stay as long as one path owns it.
Area
UI adapters — file upload field components (
FormCraft.ForMudBlazor,FormCraft.ForFluentUI) andFieldBuilderExtensions.AsFileUploadFollow-up from #328. Related: #319, #262, #326
🧠 Brainstorm
Problem / context
FormCraft's core is UI-framework-agnostic and dispatches every field through
IFieldRendererServicetoa per-framework component. Constraints travel as attributes on the field configuration, which makes
the contract between builder and component implicit: the builder writes a key, the component reads a
key, and nothing checks the two agree. When they disagree the failure is total and silent — the
constraint simply does not exist at runtime.
That is what happened here:
AsFileUploadwas changed (or written) to publish a singleFileUploadConfigurationobject, the multiple-file component was updated to read it, and thesingle-file component was not. Its code still reads the older raw keys, so it sees nothing.
Approaches
A. Patch the one component. Add a
FileUploadConfigurationread toMudBlazorFileUploadFieldComponent. Pros: smallest possible diff, fixes today's worst symptom.Cons: three other readers remain, each free to drift; the Fluent
int/longcoercion bug isuntouched; nothing prevents the fifth upload component from repeating it.
B. One shared resolution path + a parity test. Extract the resolve-config-then-fall-back-to-raw-keys
logic once, with numeric coercion, and have all four components use it. Pros: removes the class of
bug rather than an instance, and matches the invariant the profile already states for the render path;
the parity test makes a future divergence fail in CI. Cons: touches both adapters, so it is a larger
review than A.
C. Change the builder to write both spellings. Pros: no component changes. Cons: two sources of
truth that can disagree; a component reading only the raw keys still misses anything the config-only
path adds later. Moves the problem.
Recommendation
B. A is what the repo already did for presentation attributes, one at a time, across #146, #177,
#184 and #190 — the profile's Architecture grain records that history and the conclusion drawn from
it. The same conclusion applies here: the unit of work is the shared path, not the symptom. The parity
test is what makes it stick, because the failure is invisible at runtime.
📋 Spec
Goal
A constraint written with
.AsFileUpload(...)(or a raw.WithAttribute(...)) reaches every uploadcomponent identically, and a future divergence fails a test rather than shipping silently.
Scope
FileUploadConfigurationfirst and raw per-key attributes as fallback, with
int/longtolerance for size values.MudBlazorFileUploadFieldComponent,MudBlazorMultipleFileUploadComponent,FluentUIFileUploadComponentBase(and its two components) consume it..AsFileUpload(...)configuration produces the same resolvedconstraints on every upload component.
Non-goals
AsFileUploadpublic signature, or removing the raw-key fallback (breaking).values reaching the component, not about what the component then does with them.
Behaviour
flowchart TD A[".AsFileUpload(types, maxSize)"] --> B["WithAttribute('FileUploadConfiguration', config)"] B --> C{"component reads what?"} C -->|"MudBlazor multiple: FileUploadConfiguration"| D["constraints applied ✅"] C -->|"MudBlazor single: raw keys only"| E["all null → NO limits ❌"] C -->|"Fluent: raw key typed long"| F["int attribute dropped → 10MB default ❌"] G["one shared resolver:<br/>config → raw keys → coerce numerics"] --> DKey files
FormCraft/Forms/Extensions/FieldBuilderExtensions.cs—AsFileUpload(line ~299); its commentabout the component reading the attribute must become true, or be corrected.
FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs(~38-44).FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadComponent.razor.cs(~36).FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadComponentBase.cs— the existingshared base for upload components; the natural home for the resolver.
FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs(~111).Validation rules
.AsFileUpload(acceptedFileTypes: [".pdf"], maxFileSize: N)yields a non-null accept filter andMaxFileSize == Non every upload component..WithAttribute("MaxFileSize", 5_000_000)(anint) binds, and does not fall through to a default.Edge cases
intvslongis the coercion that must be handled; a boxedintfailsis long.MaxFiles = 1is set byAsFileUploadfor the single-file path — resolving the config must notlet a multiple-file component inherit a stray
MaxFiles = 1.the whole defect is a value silently not arriving.
Assumptions
FileUploadConfigurationthe only spelling, that is a deliberate breaking change and a different issue.🛠️ Implementation plan
Goal: one constraint-resolution path for upload components, so a configured limit cannot silently fail to arrive.
Architecture: UI adapters only. Core's
AsFileUploadkeeps its signature; the fix is in how components resolve what it writes.Tech stack: .NET 10 SDK pinned by
global.json(10.0.302), Blazor, MudBlazor + Fluent UI adapters, xUnit + Shouldly + bUnit.Global constraints:
dev; commit asPhilippe Matray <phmatray@gmail.com>; conventional commits.FormCraftcore stays UI-framework-agnostic — no MudBlazor/Fluent type may leak into it.TreatWarningsAsErrors=trueis deliberate — do not relax it.dotnet test --filteris inert here (MTP). Usedotnet test <csproj> -c Release -- --filter-class <FQN>, or the built host directly; see.claude/skills/repo-profile.md→ Build & test.Task 1: Pin the defect with a failing test
Files: create/extend
FormCraft.ForMudBlazor.UnitTests/Fields/FileUploadConstraintTests.cs.Interfaces: none — bUnit render assertions over
MudBlazorFileUploadFieldComponent..AsFileUpload(acceptedFileTypes: [".pdf"], maxFileSize: 5 * 1024 * 1024), render the single-file MudBlazor component, and assert the resolved accept filter and max size are the configured ones.test(mudblazor): pin that AsFileUpload constraints reach the single-file upload.Task 2: One resolver, consumed by every upload component
Files: modify
FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadComponentBase.cs(add the resolver),MudBlazorFileUploadFieldComponent.razor.cs,MudBlazorMultipleFileUploadComponent.razor.cs,FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs.Interfaces: a protected resolver on the shared upload base returning the effective
Accept/MaxFileSize/MaxFiles/ShowPreview/EnableDragDrop, resolvingFileUploadConfigurationfirst and raw attributes second.FileUploadConfiguration, fall back to the raw keys, and coerce numeric sizes so a boxedintbinds where alongis expected.MudBlazorFileUploadFieldComponent.OnInitializedat the resolver instead of its raw-key reads.MudBlazorMultipleFileUploadComponentat the same resolver, preserving its current behaviour (it is the one that works today — it must not regress).FluentUIFileUploadComponentBaseat it, removing the10L-typed fallback that dropsintvalues.FieldBuilderExtensions.AsFileUploadclaiming the component reads the configuration attribute.fix(upload): resolve constraints through one path in every upload component.Task 3: Parity guard + full verification
Files: extend the test file from Task 1 (and the Fluent equivalent under
FormCraft.ForFluentUI.UnitTests).Interfaces: none.
.AsFileUpload(...)configuration resolves to the same constraints on every upload component — the drift, not any single reading, is what must fail..WithAttribute("MaxFileSize", 5_000_000)(anint) binding rather than falling back to a default../build.sh Test(CI gate) → all suites green underTreatWarningsAsErrors.test(upload): assert every upload component resolves the same constraints.