Skip to content

Blazor static SSR SupplyParameterFromForm has issue to bind a Dictionary value which is declared after a nullable property. #56542

Open
@sgarnovsky

Description

@sgarnovsky
Contributor

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I have the following properties in a Model class:

[Column("end_date")]
[Display(Name = nameof(Resources.CommonCaptions.EndingOn), ResourceType = typeof(Resources.CommonCaptions))]
public DateTime? EndDate { get; set; }

public Dictionary<int, WeekTimeIntervals>? WeekIntervals { get; set; }

a WeekTimeIntervals declaration is:

public class TimeInterval
{
    public DateTime? From { get; set; }
    public DateTime? To { get; set; }
}

public class WeekTimeIntervals
{
    public Dictionary<int, TimeInterval>? TimeIntervals { get; set; }
}

When the EndDate value is not provided on the form, the WeekIntervals[0].Value.TimeIntervals is bind as null, even the values for the one of time interval is set.

FormMappingContext includes the following errors:

Model.EndDate
{The value '' is not valid for 'EndDate'.}
Model.WeekIntervals[1].TimeIntervals[2].From
  {The value '' is not valid for 'From'.}
Model.WeekIntervals[1].TimeIntervals[2].To
  {The value '' is not valid for 'To'.}
Model.WeekIntervals[1].TimeIntervals[0].From
  {The value '' is not valid for 'From'.}
Model.WeekIntervals[1].TimeIntervals[0].To
  {The value '' is not valid for 'To'.}
Model.WeekIntervals[1].TimeIntervals[0]
  {'Model.EndDate' does must start with 'Model.WeekIntervals[1].TimeIntervals[0]'}
Model.WeekIntervals[1].TimeIntervals
  {'Model.EndDate' does must start with 'Model.WeekIntervals[1].TimeIntervals'}
Model.WeekIntervals[1]
  {'Model.EndDate' does must start with 'Model.WeekIntervals[1]'}
Model.WeekIntervals
  {'Model.EndDate' does must start with 'Model.WeekIntervals'}

A few errors above (at the list end) look wierd ('Model.EndDate' does must start with 'Model.WeekIntervals[1].TimeIntervals[0]').

I found a reference of this error message here: FormMappingContext.cs#L139

internal void AttachParentValue(string key, object value)
    {
        if (_pendingErrors == null)
        {
            return;
        }

        for (var i = 0; i < _pendingErrors.Count; i++)
        {
            var (errorKey, error) = _pendingErrors[i];
            if (!errorKey.StartsWith(key, StringComparison.Ordinal))
            {
                throw new InvalidOperationException($"'{errorKey}' does must start with '{key}'");
            }

            error.Container = value;
        }

        _pendingErrors.Clear();
    }

I don't understand why this method is called for the "Model.EndDate" key while attaching a value to the "Model.WeekIntervals" dictionary.

If I change the properties order to make WeekIntervals to go before the EndDate property, the issue is not reproduced.

Expected Behavior

Expect WeekIntervals[0].Value.TimeIntervals has been bind to a value sent from a form correct.

Steps To Reproduce

No response

Exceptions (if any)

It is a simple NullReferenceException

.NET Version

8.0.204

Anything else?

No response

Activity

ghost added
area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates
on Jul 1, 2024
sgarnovsky

sgarnovsky commented on Oct 7, 2024

@sgarnovsky
ContributorAuthor

Created a sample app to show the issue.
BlazorFormMappingContextIssue

If empty values are posted form mapping context errors are:

The value '' is not valid for 'TestValue'.
The value '' is not valid for 'Age'.
'Model.TestValue' does must start with 'Model.Person'

The important issue here that the Container property of the last 2 errors are set to the Example type instance instead of the Person one.

Originally we thought that the issue is only appears when empty values are posted for nullable model properties. But the same issue appears in a case if some invalid value is posted.
It doesn't matter what property type is used: DateTime, int.

So, the problem is related to using a property of a complex type after a property of a simple type.

A Blazor form sample code is:

<EditForm Model="Model" FormName="Test" class="test-form">
    <div class="form-group">
        <label for="date">TestValue:</label>
        <InputNumber id="testValue" @bind-Value="Model!.TestValue" class="form-control" />
    </div>
    <div class="form-group">
        <label for="personName">Person name:</label>
        <InputText id="personName" @bind-Value="Model!.Person.Name" class="form-control" />
    </div>
    <div class="form-group">
        <label for="personAge">Person age:</label>
        <InputNumber id="personAge" @bind-Value="Model!.Person.Age" class="form-control" />
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
    <div class="form-group">
        <b>
            FormMappingContext Errors
        </b>
        <br />
        @_formMappingErros
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [CascadingParameter]
    private FormMappingContext FormMappingContext { get; set; } = default!;

    private MarkupString? _formMappingErros;

    [SupplyParameterFromForm]
    private Example? Model { get; set; }

    private ValidationMessageStore? messageStore;

    public class Example
    {
        public int? TestValue { get; set; }

        public Person Person { get; set; } = new();
    }

    public class Person
    {
        public string? Name { get; set; }

        public int? Age { get; set; }
    }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        messageStore = new(editContext);
        base.OnInitialized();

        var errors = FormMappingContext.GetAllErrors();
        _formMappingErros = new MarkupString(string.Join("<br>", errors.Select(err => string.Join(' ', err.ErrorMessages))));
    }
}

if the Person property is declared before the TestValue property - issue is not reproduced.

public class Example
    {
        public Person Person { get; set; } = new();

        public int? TestValue { get; set; }
    }

In this case, Form Mapping Context errors are generated as:

The value '' is not valid for 'Age'.
The value '' is not valid for 'TestValue'.

It looks like a bug somewhere in the Form Mapping logic.

TomSkidmore

TomSkidmore commented on Dec 6, 2024

@TomSkidmore

I'm guessing there's no movement on this one. This is something we're currently encountering. The poorly worded validation messages aside, it looks as though you simply cannot have nullable values in Blazor SSR. InputNumber and InputDate validate incorrectly if your property is a nullable, even if you set the TValue (you get the '' is not valid message)

Really poor to see no movement on this one as optional numeric and date fields are an absolute must for forms

Looks like a similarly reported issue here: #58988

sgarnovsky

sgarnovsky commented on Dec 6, 2024

@sgarnovsky
ContributorAuthor

Looks like a similarly reported issue here: #58988

this one is a bit different.

Maybe, the described issue is only a result of the nullable values binding problem.

xxnickles

xxnickles commented on Dec 10, 2024

@xxnickles

I just found this issue after opening a one related to nullable enums. For the issues I have found in this repo, I think the problem is the support of standard forms is just bad in certain areas of asp.net (Blazor and minimal API in my experience). Honestly, I think the documentation should just recommend no having nullable properties and complex objects as properties and collection that are not primitives, which also have poor support in SSR and minimal APIs as far I have been able to play with.

My workaround is not relaying on nullables, but using always defaults + validation, avoid complex properties altogether and bind everything to big POCO objects and go from there by myself. To me, it is very frustrating to find this kind of problems with forms, which is a fundamental web construct since the early days...and one would think a solved issue! Yes, I understand supporting fancy mainstream JSON is quite relevant, but forgetting the fundamentals is kind of a big red flag for a web framework

sgarnovsky

sgarnovsky commented on Apr 2, 2025

@sgarnovsky
ContributorAuthor

I've got stuck with this issue on .Net 9 again.
It looks bad as this issue is referenced to "area-mvc" label.
Actually, it is a problem with the Blazor static SSR forms binding.

A workaround is to just place complex type properties before any nullable properties in the main class declarations.

My assumption that this problem is related to the issue of binding empty string values into nullable fields. It ends up into unexpected results for the complex properties declared after an issued nullable property.

So this issue is referenced with this one: 52499
The needed fix is assumed to be applied for .Net 10 preview2. Confirmed it here: https://github.com/dotnet/aspnetcore/blob/v10.0.0-preview.2.25164.1/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs
.Net 9 last release doesn't reference these updates: https://github.com/dotnet/aspnetcore/blob/v9.0.3/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs

sgarnovsky

sgarnovsky commented on Apr 2, 2025

@sgarnovsky
ContributorAuthor

Confirmed the original issue looks fixed by an applied improvement discussed here: 52499.

One thing that it is only available starting from the .Net 10 Preview 2 now.

Will plan to close this issue after some additional testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @TomSkidmore@xxnickles@sgarnovsky

        Issue actions

          Blazor static SSR SupplyParameterFromForm has issue to bind a Dictionary value which is declared after a nullable property. · Issue #56542 · dotnet/aspnetcore