dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.

Home Page:https://asp.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Blazor 8.0 SSR throws "System.InvalidOperationException: No converter registered for type" when model class contains a constructor with parameters

dodyg opened this issue · comments

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

@page "/"

<h1>Automatic Model Binding</h1>

<div class="container">
    @((MarkupString)_showMessage)

    <br />
    <div class="row">
        <div class="col-md-6">
            <EditForm method="POST" Model="PersonInfo" FormName="info" OnValidSubmit="HandleSubmit">
                <div class="mb-3">
                    <label class="form-label" for="PersonInfo.Name">Please enter your name</label><br />
                    <InputText @bind-Value="PersonInfo.Name" class="form-control" />
                </div>
                <div class="mb-3">
                    <label for="PersonInfo.Description" class="form-label">Describe yourself</label>
                    <InputTextArea @bind-Value="PersonInfo.Description" class="form-control" />
                </div>
                <div class="mb-3">
                    <label for="PersonInfo.Hobby" class="form-label">Hobby</label>
                    <InputSelect @bind-Value="PersonInfo.Hobby" class="form-select">
                        <option>Football</option>
                        <option>Basketball</option>
                        <option>Tennis</option>
                    </InputSelect>
                </div>
                <div class="mb-3 form-check">
                    <InputCheckbox @bind-Value="PersonInfo.IsMarried" class="form-check-input" />
                    <label for="PersonInfo.IsMarried" class="form-check-label">Is married?</label>
                </div>
                <div class="mb-3 form-check">
                    @{ var idx = 0;}
                    <InputRadioGroup @bind-Value="PersonInfo.Nationality">
                        @foreach(var val in new string[] { "USA", "Indonesia", "Egypt"})
                        {
                            var id = "nationality" + idx;
                            <InputRadio class="formcheck-input" Value="@val" id="@id"/>
                            <label class="form-check-label" for="@id">@val</label>
                            <br/>
                            idx++;
                        }
                    </InputRadioGroup> 
                </div>
                <div class="mb-3">
                    <button type="submit" class="btn btn-primary">Submit</button>
                </div>
            </EditForm>
        </div>
    </div>
</div>

@code
{
    [SupplyParameterFromForm(FormName = "info")] // matches FormName="info"
    public Person PersonInfo  { get; set; } = new();

    string _showMessage = string.Empty;

    void HandleSubmit()
    {
        _showMessage = $"Name: {PersonInfo.Name}<br />Description: {PersonInfo.Description}<br />Hobby: {PersonInfo.Hobby}<br />Is married: {PersonInfo.IsMarried}<br />Nationality: {PersonInfo.Nationality}<br/><br/>";
    }

    public class Person
    {
        public string Name { get; set; } = string.Empty;

        public string Description { get; set; } = string.Empty;

        public string Hobby { get; set; } = string.Empty;

        public bool IsMarried { get; set; }

        public string Nationality { get; set; } = string.Empty;

        public Person()
        {

        }

        public Person(string name)
        {
            Name = name;
        }
    }
}

will throw because Person class contains public Person(string name) constructor. If you remove it, it will work fine.

 System.InvalidOperationException: No converter registered for type 'RazorFormHandlingTwo.Pages.Index+Person'.
         at Microsoft.AspNetCore.Components.Endpoints.FormMapping.FormDataMapperOptions.CreateConverter(Type type, FormDataMapperOptions options)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd[TArg](TKey key, Func`3 valueFactory, TArg factoryArgument)
         at Microsoft.AspNetCore.Components.Endpoints.HttpContextFormValueMapper.CanMap(Type valueType, String scopeName, String formName)
         at Microsoft.AspNetCore.Components.CascadingParameterState.GetMatchingCascadingValueSupplier(CascadingParameterInfo& info, Renderer renderer, ComponentState componentState)
         at Microsoft.AspNetCore.Components.CascadingParameterState.FindCascadingParameters(ComponentState componentState, Boolean& hasSingleDeliveryParameters)
         at Microsoft.AspNetCore.Components.Rendering.ComponentState..ctor(Renderer renderer, Int32 componentId, IComponent component, ComponentState parentComponentState)
         at Microsoft.AspNetCore.Components.Endpoints.EndpointComponentState..ctor(Renderer renderer, Int32 componentId, IComponent component, ComponentState parentComponentState)
         at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.CreateComponentState(Int32 componentId, IComponent component, ComponentState parentComponentState)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.AttachAndInitComponent(IComponent component, Int32 parentComponentId)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, Int32 frameIndex, Int32 parentComponentId)
         at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex)
         at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex)
         at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex)
         at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
         at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange`1 oldTree, ArrayRange`1 newTree)
         at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)
         at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
         at Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()
         at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
         at Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(ParameterView parameters)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(Int32 componentId, ParameterView initialParameters)
         at Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(IComponent component, ParameterView initialParameters)
         at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.RenderEndpointComponent(HttpContext httpContext, Type rootComponentType, ParameterView parameters, Boolean waitForQuiescence)         at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
         at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
         at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<<InvokeAsync>b__10_0>d.MoveNext()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Antiforgery.Internal.AntiforgeryMiddleware.InvokeAwaited(HttpContext context)
         at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

Expected Behavior

It should just work fine as input

Steps To Reproduce

Exceptions (if any)

System.InvalidOperationException: No converter registered for type 'RazorFormHandlingTwo.Pages.Index+Person'

.NET Version

8

Anything else?

No response

If you turn on debug logs, do you see a log saying "Unable to select a constructor. Multiple public constructors found for type 'RazorFormHandlingTwo.Pages.Index+Person'."? That's what I expect should be logged from here.

I think we should probably relax this check to allow the use of the default constructor if multiple constructors are available. If we want to stick with the single constructor requirement, we should emit the "Multiple public constructors found" log with a higher severity like warning.

The problem is harder to figure out if you happen to use a nested object with multiple constructors.

    public class DetailedName
    {
        public string FirstName { get; set;} = string.Empty;

        public string LastName { get; set;} = string.Empty;

        public DetailedName()
        {

        }

        public DetailedName(string firstName)
        {
            FirstName = firstName;
        }
    }

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

        public string Name { get; set; } = string.Empty;

        public string Description { get; set; } = string.Empty;

        public string Hobby { get; set; } = string.Empty;

        public bool IsMarried { get; set; }

        public string Nationality { get; set; } = string.Empty;
    }

generates

image