Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle forms with Blazor SSR #46688

Closed
5 of 24 tasks
javiercn opened this issue Feb 15, 2023 · 24 comments
Closed
5 of 24 tasks

Handle forms with Blazor SSR #46688

javiercn opened this issue Feb 15, 2023 · 24 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-full-stack-web-ui Full stack web UI with Blazor
Milestone

Comments

@javiercn
Copy link
Member

javiercn commented Feb 15, 2023

This needs design.

  • How do we map the form from the HTML to an action on the app.
  • How do we bind the form data to the app state for processing.
  • Support for antiforgery.
  • Support for server side rendered patterns (PRG).

Mapping form data specifically.

  • Declarative binding
  • Primitive values
  • Collections
  • Dictionaries
  • Complex types
  • Recursive complex types
  • Error handling
  • Globalization
  • Security
  • Extensibility
    • Ignored properties.
    • Changing the property name.
    • Required properties.
    • Custom converters for a given property.
  • Converter source generation
  • Optimization
    • Removing allocations during prefix computation.
    • Removing allocations from reading the form.
    • Optimized form data structure for common form sizes
      • Evaluate using a list instead of a dictionary for small sets of keys. AdaptiveCapacityDictionary
      • Filtering out keys we don't care about when reading the dictionary.
      • Sizing the dictionary ahead of time to avoid resizing it.
      • Inlining implementation for primitive types (bypass the converter)
      • Avoid allocating a prefix collection when there is only one dictionary on the type hierarchy (since will only process a single set of keys).
@javiercn javiercn self-assigned this Feb 15, 2023
@javiercn javiercn added area-blazor Includes: Blazor, Razor Components feature-blazor-wasm This issue is related to and / or impacts Blazor WebAssembly labels Feb 15, 2023
@javiercn javiercn added feature-full-stack-web-ui Full stack web UI with Blazor and removed feature-blazor-wasm This issue is related to and / or impacts Blazor WebAssembly labels Feb 15, 2023
@RyoukoKonpaku
Copy link
Contributor

Is it possible perhaps to link the form post to a specific method? You could possibly determine which method to wireup as an action method by looking at the edit form's @OnSubmit. This should make it a bit more intuitive and also easier to support multiple forms in a page. A similar method is supported on razor pages via the asp-page-handler="Method" tag helper.

The callback method could even allow an EditContext<TModel> parameter so the form can be validated on that method or even the usual razor pages model binding (e.g. IFormFile, IFormCollection) if you want a purely SSR component.

The model binding could check the callback parameter's TModel and bind it to any available component parameter that has the same type.

I'm imagining something like this:

<EditForm Model="@Form " method="post" OnSubmit="OnPost1">
    <InputTextArea name="review" rows="5" Value="@Form.Text" />
    <ValidationMessage For="() => Form.Text" />
</EditForm>

<EditForm Model="@Form2" method="post" OnSubmit="OnPost2">
     @* Form2 usage here *@
</EditForm>

@code {
    [Parameter] public TModel Form { get; set; }
    [Parameter] public TModel2 Form2 { get; set; }

    public async Task OnPost1(EditContext<TModel> context) 
    {
         if(context.Validated())
         {
              // Logic goes here
         }
    }

    public async Task OnPost2(EditContext<TModel2> context)
    {

    }
}

@javiercn
Copy link
Member Author

@RyoukoKonpaku thanks for the input.

We already have a similar design that I prototyped but that we have not shared yet as is still a WIP, there are some differences to what you are proposing, but the idea is similar.

@RyoukoKonpaku
Copy link
Contributor

@javiercn Great to hear, I'll definitely be looking forward for those updates.

@javiercn javiercn added this to the .NET 8 Planning milestone Feb 16, 2023
@ghost
Copy link

ghost commented Feb 16, 2023

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@mkArtakMSFT mkArtakMSFT added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Feb 16, 2023
@SteveSandersonMS SteveSandersonMS changed the title [Blazor] We can handle forms for server rendered razor component applications Blazor United: Handle forms with SSR Mar 2, 2023
@javiercn
Copy link
Member Author

Blazor forms handling

This document covers different approaches considered for handling form data within Server Rendered Blazor applications and the design considerations on the proposed approach.

Goals

  1. Provide an implementation for HTTP form handling that can work without JavaScript.
  2. Support a similar set of functionalities as ASP.NET forms for data management (a).
  3. Support progressive enhancement (or graceful degradation) with interactive components (b).
  4. Minimize or completely avoid the exposure of Blazor to HTTP specific concepts (c).

a. ASP.NET forms functionality

  • Binding form data during POST requests to properties in a component.
  • Handling multiple forms on the document.
  • Validating form data.
  • Dealing with form limits and CSRF.
  • Handling redirections.

b. Progressive enhancement

  • An interactive form can work for the most part as if it had been statically rendered in case there is an issue with interactivity.
  • A statically rendered form can automatically be enhanced when a component becomes interactive.

c. Avoid exposing Blazor to HTTP

  • For HTTP aspects we should have options to configure them at the host level and hooks that can be used to "intercep responses" in the pipeline.
  • For aspects that we want to customize on a "per request" level, we need to have our own metadata that is not tied to ASP.NET (form limits, antiforgery, for example) or provide guidance to apply the metadata conditionally with a preprocessor directive or a platform specific file.

Scenarios

1. Basic form

@page /Recipes/New
@inject NavigationManager Navigation;
@inject RecipeRepository Recipes;

<div>
<EditForm Model="@NewRecipe" OnValidSubmit="AddRecipe">
    <DataAnnotationsValidator />
    <AntiforgeryToken />

    <ValidationSummary />

    <h2>Title</h2>
    <InputText @bind-Value="@NewRecipe.Name" />
    <ValidationMessage For="@(() => NewRecipe.Name)" />

    <h2>Number of servings</h2>
    <InputNumber @bind-Value="@NewRecipe.Servings" />
    <ValidationMessage For="@(() => NewRecipe.Servings)" />

    <h2>Instructions</h2>
    <InputTextArea rows="5" name="text" placeholder="Write your instructions" @bind-Value="@NewRecipe.Instructions" />
    <ValidationMessage For="@(() => NewRecipe.Instructions)" />

    <button type="submit">Submit recipe</button>
  </EditForm>
</div>

@code {

  [SupplyFromForm] public Recipe NewRecipe { get; set; }

  public override OnInitialized() => NewRecipe ??= new Recipe();

  public async Task AddRecipe(SubmitEvent evt)
  {
    await Recipes.Add(NewRecipe);
    Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
  }
}

2. Multiple forms

@page /Recipes/Edit/{id}
@inject NavigationManager Navigation;
@inject RecipeRepository Recipes;
<div>
  <EditForm Model="@Recipe" OnValidSubmit="UpdateRecipe">
    <DataAnnotationsValidator />
    <AntiforgeryToken />

    <ValidationSummary />

    <h2>Title</h2>
    <InputText @bind-Value="@Recipe.Name" />
    <ValidationMessage For="@(() => Recipe.Name)" />

    <h2>Number of servings</h2>
    <InputNumber @bind-Value="@Recipe.Servings" />
    <ValidationMessage For="@(() => Recipe.Servings)" />

    <h2>Instructions</h2>
    <InputTextArea Name="Instructions" rows="5" name="text" placeholder="Write your instructions" @bind-Value="@Recipe.Instructions" />
    <ValidationMessage For="@(() => Recipe.Instructions)" />

    <button type="submit">Submit recipe</button>
  </EditForm>
</div>
@for (var i = 0; i < recipe.Ingredients.Count; i++)
{
  var index = i;
<div>
  <EditForm Name="RemoveIngredient" Model="@recipe.Ingredients[index]" OnValidSubmit="RemoveIngredient">
    <DataAnnotationsValidator />
    <BoundValueValidator For="IngredientId" />
    <AntiforgeryToken />

    <ValidationSummary />
    
    <ValidationRule 
      For="@(() => Recipe)"
      Rule="@(recipe => recipe.Ingredients.Any(i => i.Id == IngredientsId))"
      Message="The chosen ingredient does not exist.">

    <InputHidden Name="IngredientId" Value="recipe.Ingredients[index].Id">
    <button type="submit">Remove Ingredient</button>
  </EditForm>
</div>
}
  
@code {

  [SupplyFromForm] public Recipe Recipe { get; set; }

  [SupplyFromForm("RemoveIngredient")] public int? IngredientId { get; set; }

  [Parameter] public int Id { get; set; }

  public override OnInitializedAsync()
  {
    Recipe ??= await Recipes.GetById(Id);
    if(Recipe is null)
    {
      Navigator.NotFound();
    }
  }

  public async Task RemoveIngredient() 
  {
    Recipe.RemoveIngredient(IngredientId);
    await Recipes.Update(Recipe);
  }

  public async Task UpdateRecipe(SubmitEvent evt)
  {
    await Recipes.Update(Recipe);
    Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
  }
}

3. Handling model binding errors manually

<EditForm ...>
   <BoundValueValidator For="@Recipe" />
    ...
</EditForm>

@code{
  [SupplyFromForm(FailOnError = false)] public Recipe Recipe { get; set; }
}

Workitems

Enable binding one form per document

Form appears inside the hierarchy of the "page", there is only 1 per document.

  • Render form with antiforgery token.
  • Handle POST request
  • Register named event on renderer.
  • Add binding support (via cascading value or ParameterView extension)
  • Validate binding.
  • Trigger Submit event.
  • Emit response.

Enable multiple forms per document

Form can appear anywhere as long as is uniquely identified within the document hierarchy. Multiple forms can coexist.

  • Enable defining custom scopes.
  • Enable defining named event handlers.

Introduction to form handling in server rendered applications

Form handling is the process by which the server deals with the user submitting a form via the browser, which potentially results on some state change for the app, as well as an updated UI.

In traditional server rendered applications (like MVC and Razor Pages) the most common pattern for handling forms is POST -> Redirect -> GET. In this approach, the browser send a POST request to the server including the data in the form fields as application/x-www-form-urlencoded data (technically it's possible to also send multipart/form-data).

When the server receives the request, it processes it and either responds with some updated HTML to indicate the result (this is common when there are validation failures) or redirects the user-agent (the browser) to load a different document that usually reflects the changes that were made.

sequenceDiagram
actor User
participant Browser
participant Server
User->>+Browser: Click "send"
Browser->>Server: POST www.example.com/edit/5
deactivate Browser
alt data is invalid
activate Server
Server->>Browser: 200 OK<br><<Updated-markup-with-validation-errors>>
deactivate Server
Browser->>User: Updated document
else data is valid
activate Server
Server->>+Browser: 302 Redirect<br>Location: https://www.example.com/details/5
deactivate Server
activate Browser
Browser->>+Server: GET https://www.example.com/details/5
deactivate Browser
Server->>-Browser: 200 OK <<new-document-with-changes>>
Browser->>User: Updated document
end
Loading

Form handling patterns in interactive applications

On Blazor applications up to this day, there is no need to abide by this pattern, since the app is "stateful" it can transition between displaying data and editing it without requiring a navigation.

However, even in the case of stateful applications is very common to follow this pattern, where editing data is handled in a separate page/area of the app represented by a unique URL and after an action has been successfully performed, the app redirects the user to a different part of the app using a client-side navigation.

Even in the cases where this pattern is not followed for stateful applications, these apps implement some mechanism to deal with the "two" states (editing/reading) that are otherwise represented as separate documents. For example:

  • The app might show both the data, and a form to update it simultaneously. When the user submits data, the app re-renders and displays the updated changes.
  • The app might show a button or other interactive element that allows the user to toggle between reading the data and editing it.
    • This is also possible to achieve in server rendered apps, but it is complex to do without relying on some state to discriminate between the two UI modes. That can be done via a query string parameter, or a cookie for example.

Blazor applications can also handle richer levels of interactivity, as they are not limited to a set of static form fields when rendering a form on to the page.

On a Server Rendered Application, the server decides what fields to put in the form, and there is no way to add additional fields. That means that processing any amount of variable information requires multiple interactions with the server. For example, if we have a "recipe editor" and we want to add multiple ingredients, adding each ingredient typically requires a trip to the server to submit the new ingredient and render a new form to add more ingredients (while keeping the intermediate state around)

On a Blazor Server or Webassembly application this is not a problem since the app can manipulate the DOM dynamically and the state is preserved in memory and not serialized into the form.

  • On Blazor Server the object doesn't even need to be serialized.
  • On Blazor Webassembly the only time to serialize it is when sending it to the server for processing.

Sending form data and processing a form

The most constrained environment is Server Rendered apps where we are limited by what the browser supports.

The process for generating a form and processing it goes like this:

sequenceDiagram
actor User
participant Browser
participant Server
User->>+Browser: Types URL
Browser->>+Server: GET <<URL>>
deactivate Browser
Server->>-Browser: 200 OK <br><form action="" method="POST"><br><label for="name">Enter your name: </label><br><input type="text" name="name" id="name"><br><br><input type="submit" value="Subscribe!"><br></form>
User->>Browser: Enters name on textbox<br>Clicks Subscribe!
Browser->>+Server: POST <<URL>><br>name="<<name>>
Note right of Server: Route request to handler<br>Read data<br>Validate data<br>Process data/action<br>Generate response
Server->>Browser: 301 Redirect<br>Location:<<new-url>>
deactivate Server
Browser->>+Server: GET <<new-url>>
Server->>-Browser: 200 OK
Browser->>User: 
Loading

The contract from the server and the browser is as follows:

  • Server defines the URL and the method used to handle the form via the action and method attributes.
    • In the case above, the URL is the same as the document URL since the action parameter is empty. It could have been omitted.
    • The method is POST.
  • The server defines the keys for the data that is represented in the forms as part of the "name" attribute in the "input" elements.
    • The browser knows that it needs to gather all the "input" elements for a form and send them to the server as name value pairs in the form <<name>>="<<value>>" separated by "&".

In order for Blazor to handle forms, we need to establish two mappings:

  • One mapping between browser request and handler, between POST <<URL>> and the method that will process it.
    • This step is "routing"
  • One mapping between the form data and some value in the component.
    • This step is "model binding".

The app needs to be able to validate the incoming data, and trigger any potential action as a result of the request. That gives us the 4 steps:

  • Route to handler
  • Read data
  • Validate data
  • Process action

These steps are only necessary in passive rendering because Blazor Server can interact with the data directly and Blazor Webassembly typically uses an API call to achieve the same result.

Routing to a handler

The way Blazor normally defines a handler for a form post, is via the @onsubmit event that happens on the form element, even though, most of the time the user uses the OnValidSubmit event provided by the EditForm component.

Other frameworks like Razor pages define a method on the component that gets called by convention when a POST request arrives.

The main differences between the two approaches are:

  • Any component in Blazor can define a handler for a form post as compared to only "page" components.
    • Even if another component rendered a form, it would need to be "routed" to a handler on a page.
  • Any special method for handling the form post needs to deal with a separate way of reconstructing the state of the app and validating the data.
    • Validation in Blazor is defined inside the form, which means it is done imperatively, and not declaratively, so it is not easy to replicate on a separate method.

The common approach for server rendered technologies is that when a request is submitted, an instance of the handler is created, data is bound, and then the app renders a response.

It seems clear then, that given the constraints above (limitting forms to pages being the least important). We want to follow a process similar to:

  • Receive request.
  • Render the application.
  • Bind the data to a rendered component.
  • Wait for quiescence.
  • Trigger form event (which in turn will trigger the validation).
  • Run the event handler and produce a UI update.

We can choose to have a special method to specifically handle form posts, but it needs to be bound to the form via an attribute that gets populated during render. Otherwise, we end up duplicating logic for validation and so on. If we can live with the @onsubmit event, that seems better.

It is clear then that we need to establish a mapping between the form tags and the @onsubmit events that are associated with them.

We can think of compile time generating an identifier for each form that we render, but that will make it impossible to render more than one instance of a component with that form into the page, and won't allow libraries to develop form components.

The alternative is to give the form a name that is controlled by the app developer. This could be achieved with the name attribute on the form element or with a directive like @onsubmit:name.

With that, we are able to identify the form, but we still need a way to express that in HTML so that the information can be sent during the POST request to the server.

There are a couple of ways in which we can do that:

  • Using a hidden field on the body:
    • Like x-blazor-handler. This has the drawback that we need to read the body of the request to identify the handler, and is not friendly to other middlewares analyzing the request pipeline. In addition to that, it only works for POST requests.
  • Using a query string parameter.
    • This is the way razor pages does it, with the ?handler parameter.

With all these pieces in place, we can establish a mapping between the client and the server in the following way:

<form 
+  action="?handler=myHandler"
   method="POST" 
   @onsubmit="OnSubmit"
+  @onsubmit:name="myHandler"
>
...
</form>

That is the raw way of achieve the mapping with a plain form, but edit form can do a bit better:

<EditForm Name="myHandler" OnValidSubmit="ProcessForm">
</EditForm>

which internally translates to

<form 
+  action="?handler=myHandler"
   method="POST"
   @onsubmit="OnEditFormSubmit"
+  @onsubmit:name="myHandler"
>
...
</form>

We still need to have a way to make these identifiers unique, but we are going to tackle that problem separately.

When to bind form data

The other part that we need to tackle is establishing a mapping between the data in the form and some state on the server. This problem is generally referred to as "model binding".

In the common case for a Blazor application, data is populated during the first render of a component, within the OnInitialized(async) method call.

We want model binding to happen at that stage, otherwise we force the app to load the data unnecessarily.

There are several approaches we can use to define how we bind the data:

  • We can use a "BinderService" that gets injected on to the page.
    • The user would call data = service.Bind<TypeToBind>(...) during OnInitialize and if that doesn't work, would populate the data from the database.
  • We can use a special component <FormBodyProvider> that wraps the rendered page alongside a [SupplyParameterFromBody] to provide the data as a parameter. FormBodyProvider would inspect the type, determine the parameters and pass those to the page.
    • This approach is only limited to the rendered page, similar to query string parameters.
  • We could bind the data to the OnSubmit event. Something like public OnFormSubmit(SubmitEvent<TFormData> data)
    • The problem here is that the app tries to load the data before the event gets triggered.
      • The app would need a special gesture to know that it needs to avoid loading the data during the OnInitialize method so that it can be processed on the event.

Based on this, it is clear that binding needs to happen while the app is rendering, and before we process the action (by triggering the form event) so that all the components that can influence the processing of the data can be instantiated.

That leaves us with two options:

  • <FormBodyProvider> which populates the data as a parameter to the component.
  • ModelBinderService which allow us to programatically bind the data.

Actually, there is a third option that we have not considered. This third option is a CascadingValue.

Cascading values are a way to provide context to a component "from outside". Typically an ancestor component. This approach is a combination of the two approaches above:

  • The value comes in as a parameter (similar to FormBodyProvider) but it can be used on any ancestor.
  • The value is "injected" into the component during the component lifecycle (Similar to how ModelBinderService would be called during OnInitialized(Async)).

There is one problem though. We don't have an instance of the value to inject, we only have form data, and we would need to process that data before we can set the value.

Finally, one aspect that we have not considered but that also applies to binding when multiple forms are rendered, is that we need to selectively bind the data only to the component containing the form that triggered a request. Other forms should just load their data through their regular mechanism without trying to bind it.

Since this data is typically loaded within the components, we also need to ask ourselves the question of whether this data is a Parameter or part of the component state. (Can it be seen as an optional parameter with a default value?).

How to bind form data

This is the process by which we establish a mapping between the data sent in the POST request (the form URL encoded data) and the members in the type we want to "bind" the data to. This problem is equivalent to deserializing other formats like JSON or XML, and the same constraints and concerns apply. The main aspect we need to take into account is how we establish the mapping between the form data and the value that we want to populate.

There are a set of known of primitive types that get mapped directly (pretty much anything that implements IParseable).

Then we have to think about complex types:

  • Classes
  • Structs

And we need to think about collections:

  • Arrays
  • Lists
  • Dictionaries

We can choose how complex this mapping can be, as we can always extend it in the future without introducing a breaking change.

Since forms only support key value pairs in the form of "name"="value", we are forced to use the "object path" as the name for every leaf primitive value that we want to set.

For example, given the following code:

public class Customer
{
  public Address Address { get; set; }
}

public class Address 
{
  public string Street;
  public string Province;
  public string Country;
}

The "object paths" in the form would be like:

  • Customer.Address.Street="street"
  • Customer.Address.Province="province"
  • Customer.Address.Country="country"

If we decide to do collections, the syntax that is already supported by MVC and Razor Pages is as follows:

  • Property[Index] for primitive type collections
  • Property[Index].OtherProperty for complex type collections

Note that the index can be a number for IEnumerable collection types or a string for dictionary types.

When binding collections the Indexes must be consecutive, meaning there can't be gaps between them (must check this).

The way we specify these values is via the name attribute on the input fields. So for example:

  • <input type="text" name="Customer.Address.Street" ...>
  • <InputText Name="Customer.Address.Street" .../>

Our components can do better though, since most of them support @bind we can leverage the passed in expression to populate the name without an explicit gesture:

  • <InputText @bind-Value="customer.Address.Street" .../>

Writing complex forms split across components

The approach described works so far when all the inputs defined in the form are defined explicitly on the same component. However, in complex forms, it is common to split the form definition into multiple subcomponents that get rendered inside the form.

In our recipe editor app, this is the case for the ingredients list component. The problem is that at the point we render that component, we loose track of the "prefix" that needs to be used for the name field.

This means that the developer needs to pass this prefix explicitly to the component and the component needs to take it into account to preprend it to any other input element rendered inside the child component.

In Razor (for MVC and Razor Pages), the ViewData dictionary contains a "TemplateInfo" object that propagates this prefix when rendering elements in partial views, tag helpers, etc.

We can choose to support a similar concept in Blazor using a cascading value instead provided by a special component EditGroup

<EditGroup For="customer.Address">
  <AddressEditor @bind-Value="customer.Address">
</EditGroup>

And inside AddressEditor:

<InputText @bind-Value="value.Street" />

Which will produce the result:

<input name="customer.Address.Street" value="" />

The reason this is needed in server rendered applications and is not needed in Blazor Server or Webassembly is because in the latter two cases, you always maintain "referential integrity" between the input element and the data associated with the form, while on server rendered apps, that's not the case. Hence why we need to use the "name" property in the input element, and why input elements need to be "context aware" instead of relying on the pure bindings.

Note that we can choose to not build EditGroup alltogether and force the user to be explicit about the prefix when rendering form elements inside children components.

Form handling a complete example

With the two aspects described above, lets look at how a component can potentially look:

Recipe.razor

@page /Recipe/Edit/{id}
<div>
    <h1>Recipe Editor</h1>

    <p>Share your great recipes with the Best For You community.</p>

    <EditForm Name="EditRecipe" Model="@recipe" OnValidSubmit="HandleSubmit">
        <DataAnnotationsValidator />

        <h2>Title</h2>
        <InputText Name="Name" @bind-Value="@recipe.Name" />
        <ValidationMessage For="@(() => recipe.Name)" />

        <h2>Number of servings</h2>
        <InputNumber Name="Servings" @bind-Value="@recipe.Servings" />
        <ValidationMessage For="@(() => recipe.Servings)" />

        <EditGroup Name="recipe.Ingredients">
          <h2>
            <IngredientsListEditor @bind-Ingredients="@recipe.Ingredients" />
          </h2>
        <EditGroup>
        <ValidationMessage For="@(() => recipe.Ingredients)" />

        <h2>Instructions</h2>
        <InputTextArea Name="Instructions" rows="5" name="text" placeholder="Write your instructions" @bind-Value="@recipe.Instructions" />
        <ValidationMessage For="@(() => recipe.Instructions)" />

        <button type="submit">Submit recipe</button>
    </EditForm>
</div>

@code {

    [Parameter] public string Id { get; set; }

    // Option 1: Requires some component or runtime support to bind the value 
    [SupplyFromBody] public Recipe Recipe { get; set; }

    // Option 2: Requires framework integration
    [CascadingParameter(Source=FromBody)] public Recipe Recipe { get; set; }

    public void OnIntializedAsync()
    {
      Recipe ??= await Repository.LoadRecipe(Id);
    }

    async Task HandleSubmit()
    {
        recipe.Tags = new[] { "TODO" };
        recipe.Id = await RecipesStore.UpdateRecipe(recipe);
        Nav.NavigateTo($"recipe/{recipe.Id}");
    }
}

Disambiguating forms

When there is more than one form on the page, we need to decide what part of the application gets to bind data and what form gets to handle the event.

For forms, the event will come to a url with a query string parameter ?handler="<<handler-name>>" that will be associated with the event.

For the property the form needs to bind to, there is no association that maps a concrete handler to a property.

However, we can use the component hierarchy to disambiguate. For example, imagine we have an app, where there are two forms.

One on the main page, and another one in a side bar menu. We can give each "section/area" a name, like "main" and "navigation".

We can cascade those names down the component hierarchy.

Components can consume those names and use them for uniquely identifying data to bind to and form handlers.

This removes the need for our forms to explicitly use a name when they are inside a section.

If we put all the components on a single file it would look like this:

<Section Name="navigation">
<!-- Imagine this is inside NavMenu.Razor -->
<!-- Imagine there is an EditMenuModel property defined with the right attributes to trigger binding -->
<EditForm Model="EditMenuModel" OnValidSubmit="NavMenuSubmit">
...
</EditForm>
</Section>
<Section Name="main">
<!-- Imagine this is inside Index.Razor or other page -->
<!-- Imagine there is an CustomerModel property defined with the right attributes to trigger binding -->
<EditForm Model="CustomerModel" OnValidSubmit="AddCustomer">
...
</EditForm>
</Section>

This would translate to:

<!-- Content inside NavMenu.razor -->
<form action=?handler="navigation" method="POST">
...
</form>

<!-- Content inside Index.razor -->
<form action=?handler="main" method="POST">
...
</form>

With this in mind, we have the final mapping that we needed, which is the "handler" is associated with the property to be "bound" to, via the scope.

There are two important considerations here:

  • You can still define a name for a property and a handler if you have multiple forms in the same scope (so you disambiguate).
  • The app developer (and not who wrote the component) can always disambiguate by adding a scope to the "scope chain".
  • For the most part, we don't expect this mechanism to be something users reach on to on a regular basis. Most cases will involve 1 form per page, and we will have a default scope that gets rendered by the RouteView, so as long as there is only one form in your page, you should be good.

Streaming rendering

When a component opts-in for streaming rendering, we don't expect the streaming to start until the first render happens after we've dispatched the event.

The application needs to achieve quiescence before we dispatch the event, to ensure that the form component has been rendered and any required value has been bound.

Once we invoke the handler, we want to start the streaming process after the dispatched event produces a render. This allows the app to display intermediate states while processing the form. It can for example, disable the form during the processing of the event, and it can display progress.

There are considerations to be taken into account with regards to antiforgery, see the details on "Security considerations".

Redirecting

It is going to be relatively common that we need to redirect the browser after successfully handling an action. We could do this in two ways:

  • If we have not started streaming rendering, we will wait for quiescence, tear down the renderer and redirect.
  • If we have started streaming rendering, we will navigate internally (url will not update), we will render the new page, wait for quiescence, and then from the client we will update the browser URL client-side with all the navigation entries.
  • If we determine that the URL is outside of the app, we will wait for quiescence, and then trigger the navigation (from the client).

Error handling

There are several classes of errors that are unique to the way Server Rendered apps work. Server rendered apps introduce new failure modes in the form of:

  • Failed to bind the request to the component.
  • Failed to validate security constraints like antiforgery.
  • Failed to respect form limits imposed by the server.

These types of errors are in many cases an application error or the result of an exceptional situation. We need to have a way to express these concepts in server rendered applications.

There are several ways in which we can do this:

  • A component: You add the component to your form and it works as any other validation element would.
    • It has a way of checking the binding state for the elements associated with the form and can produce errors. During initialization, the bound values simply receive their default values.
  • An exception that is thrown during binding if it was not possible to bind the data.
  • A result that is returned from the binding process that indicates whether binding succeeded or failed and that can be used to make rendering decisions further down the component life cycle.

Some security constraints need to be validated before we try to process the form, like the form limits or antiforgery, as a result these need to be enforced before form is even rendered and might need to result in a "hard failure". Meaning that we don't even render the page, and we send back a 400.

For determining whether the model was correctly bound, the approach MVC uses is to avoid producing an error in the first place, and force the user to check the ModelState in the controller page.

The equivalents in Components is the EditContext, but this only gets populated after we have rendered the form at least once. If we use a component, the app has no way within OnInitialized to know whether the binding process failed or succeeded. This is generally not a problem unless we want to avoid having the app use a fallback, like loading the data from the database.

Security considerations

The security checks need to be performed in a specific order. For example, form limits need to be applied before we try to validate an antiforgery tokens.

We need to be able to define form limits on a "per form" basis, as the global default might not work for a specific example.

It is important to note that a well functioning app should never fail because the form limits have been exceeded. There should be validation rules on the app to prevent this siutation.

Antiforgery

Antiforgery and streaming rendering need to work together. During get requests, the antiforgery token needs to be appended as a cookie to the response. Regularly, the antiforgery token needs to be updated (to refresh it) or because the user identity changed as part of a login.

We can choose to always attach the antiforgery token to the response before start streaming any response.

  • That comes at the cost of adding the antiforgery token even if we don't need it.

We can choose to have a way to signal this requirement on a per endpoint level.

  • Via an attribute on the @page component.
  • This won't work for forms rendered outside of the main page.

We can do nothing and throw:

  • We can consider that this is a programming issue and that you need to make sure you attach the antiforgery token before any streaming rendering happens.

Modelbinding limits

Since we are binding data on the server, we need to account for limits in terms of how much "recursion" we are willing to do (how much we want to dive into nested structures to perform deserialization) and how big can the collections we are binding be.

See here for more details

Open questions

  • Do we need a "Global default" for form limits on Razor Component Apps or can we live with the "global system default". (Is there such a thing?).
  • Do we want to have an "extended" EditForm that includes the security elements (like antiforgery, checking the binding status, etc)

@javiercn
Copy link
Member Author

Summary

Provide support for handling server side rendered forms within Razor Component Applications as part of "Blazor United".

Motivation and goals

Blazor united apps provide a unified model for authoring web applications whether the app is server rendered or a SPA. Handling form data is part of such experience. The only way customers have to provide a "server rendered only" experience with components requires using MVC or Razor Pages. We want to make it easier for customers to reach a wider customer base with the same code.

  1. Developers can write and process forms in a Server Rendered Blazor Application.
  2. Developers can author server rendered forms using the same concepts they know for interactive forms.

In scope

  1. Render a single form in the app with a fixed set of fields and process its submision for a model that contains primitive values or is not a recursively defined type inside the scope of a Page component.
  2. Render multiple forms via named handlers.
  3. Render forms outside the page component scope.

Out of scope

  • Rendering recursively defined types. E.g:
    public class MyType { public MyType Property { get; set; }}
    • This drastically simplifies modelbinding

Risks / unknowns

Covered (hopefully) in the big doc.

Detailed design

Scenario 1

@page /Recipes/New
@inject NavigationManager Navigation;
@inject RecipeRepository Recipes;

<div>
<EditForm Model="@NewRecipe" OnValidSubmit="AddRecipe">
    <DataAnnotationsValidator />
    <AntiforgeryToken />

    <ValidationSummary />

    <h2>Title</h2>
    <InputText @bind-Value="@NewRecipe.Name" />
    <ValidationMessage For="@(() => NewRecipe.Name)" />

    <h2>Number of servings</h2>
    <InputNumber @bind-Value="@NewRecipe.Servings" />
    <ValidationMessage For="@(() => NewRecipe.Servings)" />

    <h2>Instructions</h2>
    <InputTextArea rows="5" name="text" placeholder="Write your instructions" @bind-Value="@NewRecipe.Instructions" />
    <ValidationMessage For="@(() => NewRecipe.Instructions)" />

    <button type="submit">Submit recipe</button>
  </EditForm>
</div>

@code {
  [SupplyFromForm] public Recipe NewRecipe { get; set; }

  public override OnInitialized() => NewRecipe ??= new Recipe();

  public async Task AddRecipe(SubmitEvent evt)
  {
    await Recipes.Add(NewRecipe);
    Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
  }
}

Steps:

  1. There is a ModelBinder cascading value that flows from RouteView.
    • Let's say that we need to define a property to bind it to begin with, even though we are not showing it here (we can hopefully tweak this in the renderer to avoid having to define a property).
  2. When the app receives a GET request to render the form, EditForm renders the form tag and the fields.
    • The antiforgery component takes care of rendering the body piece of the token inside a hidden field.
  3. The user fills in the form data and hits submit.
    • The browser sends a POST request with the form data in the body.
  4. When we receive a POST request, we begin rendering the app.
    • Within ComponentBase, during the initial call to SetParametersAsync and after having set the parameters to the component, we try to model bind the properties annotated with the [SupplyFromForm] attribute.
  5. After that, OnInitialized(Async) runs, and we can check if there is already a value or otherwise, we create a new model.
  6. The app renders, which in turn renders the form into the page.
    • We wait for quiescence, to make sure that all the components have had a chance to get initialized.
  7. We dispatch the submit event to the renderer, which in turn triggers the form validation.
    • If the validation passes, then we invoke OnValidSubmit and the app gets to save the data to the database. Once its done, it can redirect to a new location.
    • If the validation fails, we re-render the form and continue sending updates until we reach quiescence.
      • Note that EditForm can receive the ModelBinder instance as a cascading value, which enables it to print any message that happens as a result of a failure to bind the model.

Work items

  1. Define the ModelBinder that is responsible for binding the form to an annotated property in a component.
  2. Update component base to bind annotated properties for a given component before OnInitialized(Async) runs.
  3. Update EditForm to register a handler for the submit event of the form element with the renderer (via a new RenderTreeBuilder API).
  4. Update EditForm to receive a ModelBinder instance and populate errors as part of validation.
  5. Update EditForm to default to POST as GET is not suitable for most scenarios.
  6. Update InputBase to generate the Name for the form field based on the ValueExpression if one has not been provided.
  7. Add a new endpoint to handle POST requests (or add a new POST specific endpoint).
  8. Within the POST endpoint, perform form limit validation, antiforgery validation, and any other relevant security check, before we try to bind any form data.
  9. Within the POST endpoint, render the component, wait for quiescence and then dispatch the submit event associated with the registered event to the renderer.
  10. Update NavigationManager to support redirects after streaming rendering has started.
  11. Provide an antiforgery component and an antiforgery service responsible for rendering a token within the page.

Scenario 2

<ul>
@for(var i = 0; i < recipe.Ingredients.Count; i++)
{
  var index = i;
  <li>
    <span>@Ingredient.Description - @Ingredient.Ammount</span>
    <Scope Name="@recipe.Ingredients[index]">
      <RemoveIngredient Value="@ingredient">
    </Scope>
  </li>
}
</ul>

RemoveIngredient.razor

<EditForm Model="@Ingredient" OnValidSubmit="RaiseOnRemoveIngredient">
<!-- Renders <form action=?handler="<<scope-chain>>" ...>  -->
    <DataAnnotationsValidator />
    <AntiforgeryToken />

    <ValidationSummary />
    <InputHidden Value="@Ingredient.Id">
    <button type="submit">Remove ingredient</button>
</EditForm>

@code 
{
  [Parameter] public Ingredient Ingredient { get; set; }
  [Parameter] public EventCallback<int> OnRemoveIngredient { get; set; }
  [SupplyValueFromForm] public int Id { get; set; }

  public Task RaiseOnRemoveIngredient(){
    return OnRemoveIngredient.InvokeAsync(Id);
  }
}

Steps:

  1. Compute the "current scope" by chaining all the <Scope> cascading values in the hierarchy. Note that <Scope is a placeholder term.
  2. Update EditForm to include a ?handler parameter that includes the Scope chain.
  3. Update the ModelBinder to take into account the Scope and bind only when computing the Handler it applies to.
  4. Define the Scope so that it can be used in the app.

Scenario 3

<div>
<EditForm 
+ Handler="@nameof(EditRecipe)
  Model="@Recipe"
  OnValidSubmit="EditRecipe">
    <DataAnnotationsValidator />
    <AntiforgeryToken />
    <ValidationSummary />

    <h2>Title</h2>
    <InputText @bind-Value="@Recipe.Name" />
    <ValidationMessage For="@(() => Recipe.Name)" />

    <h2>Number of servings</h2>
    <InputNumber @bind-Value="@Recipe.Servings" />
    <ValidationMessage For="@(() => Recipe.Servings)" />

    <h2>Instructions</h2>
    <InputTextArea rows="5" name="text" placeholder="Write your instructions" @bind-Value="@Recipe.Instructions" />
    <ValidationMessage For="@(() => Recipe.Instructions)" />

    <button type="submit">Submit recipe</button>
  </EditForm>
</div>
+<!-- Generates <form action="?handler="EditRecipe"> -->

@code {
+ [SupplyFromForm(nameof(EditRecipe))] 
  public Recipe Recipe { get; set; }

  [Parameter] public int Id { get; set; }

  public async Task override OnInitializedAsync() => Recipe ??= await Recipes.GetById(Id);

  public async Task EditRecipe(SubmitEvent evt)
  {
    Recipe.Id = Id;
    await Recipes.Update(Recipe);
    Navigator.NavigateTo($"/Recipes/{Recipe.Id}");
  }
}

Workitems

  1. Include the Handler in the action parameter as a query string parameter.
  2. Provide the Handler to the ModelBinder when the POST endpoint receives the request.
  3. Use the provided Handler to discriminate which properties to bind.
  4. Use the provided Handler to discriminate what forms must receive errors.

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Mar 23, 2023

There is a ModelBinder cascading value that flows from RouteView.

Is this a new type, or something from MVC model binding? I'm guessing new type for layering.

Let's say that we need to define a property to bind it to begin with, even though we are not showing it here (we can hopefully tweak this in the renderer to avoid having to define a property).

Could you rephrase? I don't know what this remark is about at all.

Update component base to bind annotated properties for a given component before OnInitialized(Async) runs.

If possible let's explore if we can make this be supplied as an ICascadingValueComponent so that it naturally works through the existing cascading value system, and no new implementation is needed on ComponentBase, and it would even work with pure IComponent.

Update EditForm to register a handler for the submit event of the form element with the renderer (via a new RenderTreeBuilder API).

We may be able to avoid having a new renderer API for this. The prerenderer already gets RenderBatch instances with the event handler info, so it can search those to find the onsubmit handler corresponding to a form with the desired name.

Update EditForm to receive a ModelBinder instance and populate errors as part of validation.

Sounds reasonable that EditForm acts as the recipient of the cascaded value. If possible, most of the implementation should really be inside EditContext since up until now EditForm hasn't been magic in any way (it's something you could implement in userland code). It might work out quite nicely to do this in the same way that EnableDataAnnotationsValidation works, i.e., ModelBinder knows how to register itself with an EditContext for a callback on validation and it will be able to attach validation messages.

Within the POST endpoint, perform form limit validation, antiforgery validation,

I'm not sure how the endpoint knows whether or not you have antiforgery validation enabled. Should the enforcement perhaps be inside the <AntiforgeryToken> itself? (i.e., if it can somehow know if the current context is a non-GET request, then it can fail hard with an unhandled exception if the token is missing or wrong). I'm not worried about how this is done, just writing this to track that it's not obvious to me where the enforcement would go.

Within the POST endpoint, render the component, wait for quiescence

I'm not sure we need to wait for quiescence. In almost all cases the form will be rendered synchronously anyway, and if there's some completely unrelated component elsewhere in the hierarchy that's still loading stuff, why would we want to wait for that? Would anything bad happen if we don't wait for quiescence?

Within the POST endpoint, render the component, wait for quiescence and then dispatch the submit event associated with the registered event to the renderer.

Which code actually is responsible for dispatching this event? Presumably something in the passive rendering logic. Is that right?

Update NavigationManager to support redirects after streaming rendering has started.

I think that can be regarded as unrelated to this work. It's a feature we need independently of form post handling.

Define the Scope so that it can be used in the app.

If at all possible I'd love to avoid introducing the <Scope> concept at this stage as it seems far too general (especially its name) and is only required for an edge case. It's good to have it on the backlog as a possible extension in the future but it seems like a clear candidate to be avoided for MVP. The vast majority of scenarios will not require this (including multi-form cases, as in many cases they will all map to a single named action anyway).

Handler="@nameof(EditRecipe)

I think we need a more specific name - handler could mean anything. Maybe FormName?

Include the Handler in the action parameter as a query string parameter.

If it's a form with method=post, could we default to doing it as a hidden input instead of querystring? Doing a querystring is complicated because we have to combine with other query params and it shows up in the UI to end users, which developers might not want.

Also should we have an error if more than one form matches the specified handler/name? That would catch issues where otherwise you might not notice that multiple @onsubmit events were firing.

@SteveSandersonMS
Copy link
Member

BTW sorry that sounds like a long list of objections/complaints! I think this is good design work and am only posting all this stuff as a big block to get the risk areas out of the way early and hopefully leave you as unblocked as possible as soon as possible.

@javiercn
Copy link
Member Author

Is this a new type, or something from MVC model binding? I'm guessing new type for layering.

Yes, this is a new type defined for Components. I am hoping that by default this can be kept "hidden" unless you explicitly ask for it via a [CascadingValue] property.

Could you rephrase? I don't know what this remark is about at all.

Update component base to bind annotated properties for a given component before OnInitialized(Async) runs.

If possible let's explore if we can make this be supplied as an ICascadingValueComponent so that it naturally works through the existing cascading value system, and no new implementation is needed on ComponentBase, and it would even work with pure IComponent.

Update EditForm to register a handler for the submit event of the form element with the renderer (via a new RenderTreeBuilder API).

Yes, my idea is that this goes through ICascadingValueComponent in the end, I was mentioning here that to begin with, we'll do this with a regular cascading value (more scoped) to avoid having to tune the cascading value code. But the idea is that [SupplyFromBody] becomes similar to [CascadingValue]. Just using cascading value to begin with, it's simpler to validate the idea.

We may be able to avoid having a new renderer API for this. The prerenderer already gets RenderBatch instances with the event handler info, so it can search those to find the onsubmit handler corresponding to a form with the desired name.

Registering the event name is for performance, to avoid the search through the render batch. I expect us to be able to do this pretty efficiently.

Sounds reasonable that EditForm acts as the recipient of the cascaded value. If possible, most of the implementation should really be inside EditContext since up until now EditForm hasn't been magic in any way (it's something you could implement in userland code). It might work out quite nicely to do this in the same way that EnableDataAnnotationsValidation works, i.e., ModelBinder knows how to register itself with an EditContext for a callback on validation and it will be able to attach validation messages.

This is the idea, EditForm retrieves the BindingContext and just calls a method to populate the EditContext with it values. EditForm just "orchestrates".

I'm not sure how the endpoint knows whether or not you have antiforgery validation enabled. Should the enforcement perhaps be inside the <AntiforgeryToken> itself? (i.e., if it can somehow know if the current context is a non-GET request, then it can fail hard with an unhandled exception if the token is missing or wrong). I'm not worried about how this is done, just writing this to track that it's not obvious to me where the enforcement would go.

The enforcement needs to happen before model binding happens. We have some options described above, like making the check always within a POST request, and in addition we can have an attribute that you apply to the thing you are binding to optionally skip it (but should be on by default). Based on experience with Razor Pages, having antiforgery on by default has not been an issue and it's a much safer default.

<AntiforgeryToken /> should just deal with emitting the request token and not doing any validation, as its to late when this has already rendered.

I'm not sure we need to wait for quiescence. In almost all cases the form will be rendered synchronously anyway, and if there's some completely unrelated component elsewhere in the hierarchy that's still loading stuff, why would we want to wait for that? Would anything bad happen if we don't wait for quiescence?

Waiting for quiescence is the best guarantee we have that the app is in as close state as it was when it rendered during the initial request. While it might be true that the form will render synchronously most of the time, we can't be sure, so it is safer to wait by default and maybe provide an option.

As an alternative, we can register the named event during the initial call to start the render, and the renderer can then automatically fire it the moment it sees a component define the named event.

That actually makes it even better, because it means that we don't need to keep track of named events. We can just have one slot for the event we are processing on the renderer and the moment we see it defined, we can queue a call to dispatch it.

Which code actually is responsible for dispatching this event? Presumably something in the passive rendering logic. Is that right?

Yes, the endpoint triggers the dispatch of the named event via the passive renderer.

I think that can be regarded as unrelated to this work. It's a feature we need independently of form post handling.

Yep, I am aware, just putting it here so that we don't lose track of it.

If at all possible I'd love to avoid introducing the <Scope> concept at this stage as it seems far too general (especially its name) and is only required for an edge case. It's good to have it on the backlog as a possible extension in the future but it seems like a clear candidate to be avoided for MVP. The vast majority of scenarios will not require this (including multi-form cases, as in many cases they will all map to a single named action anyway).

I chose "scope" just to be clear about what it does, but this idea comes from the improved persistence in the prototype. Essentially, it's the same as the "PersistentBoundary" here when/if we get to that bit, I want that concept to also be usable here to disambiguate.

This is also done as a component, but we can potentially find a different way to do it that does not involve an extra component. It's just more work. For example, it could be a directive `@scope="@(<>)" that we convert into a "special" value that we can understand and cascade through the rest of the hierarchy.

Hope that makes sense

I think we need a more specific name - handler could mean anything. Maybe FormName?

I used Handler just to mimic the razor pages term. We can use Name, but I will prefer we avoid it so as not to stomp on the actual name of the form if the user wants to set it. The final name is to be determined, just the concept is what we need.

If it's a form with method=post, could we default to doing it as a hidden input instead of querystring? Doing a querystring is complicated because we have to combine with other query params and it shows up in the UI to end users, which developers might not want.

Hidden has problems. It's not visible to other parts of the stack without reading the body, which is generally frowned upon, as you want to do security validation before you read the body.

I can sympathize with the handler appearing in the UI, but the reality is that it's the only thing that works with both GET and POST forms and that can "safely" be read by other parts of the stack. We can't use the path because otherwise people can't use catch-all parameters on their routes, and we want the handler being invoked to be clearly identifiable in the server logs and other things like that, that won't read the body.

Once we do the "enhanced mode" we can put the handler in a custom request header, and that way we avoid the UI problem. Would that work? (The thing that processes the "enhanced" form just strips the query parameter from the action and send it on the request header instead)

Also should we have an error if more than one form matches the specified handler/name? That would catch issues where otherwise you might not notice that multiple @onsubmit events were firing.

Yes, the event name should be unique across the entire component hierarchy. We can probably validate this if we see more than one component try to register the same name during the render as part of the "POST" request. At which point we can choose to throw an exception.

@danroth27
Copy link
Member

When the app receives a GET request to render the form, EditForm renders the form tag and the fields.

Can we clarify here in the proposal what gets rendered in the form for the action value by default?

@danroth27
Copy link
Member

After that, OnInitialized(Async) runs, and we can check if there is already a value or otherwise, we create a new model.

The "we" here makes it sound like the framework will do this, but this is just user code, correct?

@danroth27 danroth27 changed the title Blazor United: Handle forms with SSR Handle forms with Blazor SSR Apr 24, 2023
@javiercn
Copy link
Member Author

Form binding for Blazor SSR apps

The goal is to enable Blazor SSR apps to handle forms in a natural way similar to how Razor Pages and MVC apps do so.

Upon receiving a POST request, we identify what area of the page (if any) is requesting the data to be bound as a parameter.

A user specifies this by using the [SupplyParameterFromForm] attribute on a property in the component. From there on, we combine
the name (if provided) in [SupplyParameterFromForm] with the current form handling context name, and if they match, we proceed
to try and bind that value. Some examples:

Who gets to do binding

There is only one property in the entire component hierarchy that gets to bind data using [SupplyParameterFromForm] and that property should be associated with the form that is handling the post request as described above. We decide who gets to bind using the same criteria we use for forms. Some examples are:

Form inside a page

+<EditForm Model="Customer" OnValidSubmit="AddCustomer" novalidate>
    <DataAnnotationsValidator />
    <div>
        <ValidationSummary />
    </div>
    <div>
        <label for="Name">Name</label>
        <InputText @bind-Value="Customer.Name" />
        <ValidationMessage For="() => Customer.Name" />
    </div>
    <div>
        <label for="Age">Age</label>
        <input type="text" name="Age" value="@Customer.Age" />
        <ValidationMessage For="() => Customer.Age" />
    </div>
    <div>
        <input type="submit" value="Add Customer" />
    </div>
</EditForm>

@code {

    private bool _customerAdded;
+    [SupplyParameterFromForm] public CustomerDto Customer { get; set; }
}

Form with a name

<EditForm 
+  FormHandlerName="Handler"
   Model="Customer" OnValidSubmit="AddCustomer" novalidate>
    ...
</EditForm>

@code {
+    [SupplyParameterFromForm(Name="Handler")] public CustomerDto Customer { get; set; }
}

SupplyParameterFromForm is aware of the context (same as EditForm) provided by CascadingModelBinder so wrapping the form and the parameter in a CascadingModelBinder instance with a name, can be used to disambiguate from other forms. This can be done by component libraries themselves, or by the application developer in case they need to do so.

Binding conventions

As we said, there is only one property in the component hierarchy that gets to bind data. There are two cases that we care about when binding properties:

  • We are binding a single value (something that can be bound directly from a string).
    • The convention is to bind a field with a name "value".
  • We are binding a complex value (an object, collection, dictionary, etc.) that is represented as multiple fields.
    • The fields are represented by the object paths to the leaf values of that complex object from the root of the app.

For example, for a Customer type with an Address property, the paths are represented as Address.Street, Address.ZipCode, etc.
Collections and dictionary indexes are supported and use the [<<index|key>>] syntax.

This convention is intentionally compatible with the MVC and Razor Pages convention to ease migration scenarios and support using component forms in a natural way inside Razor Pages and MVC.

Binding process

There are 4 cases we care about:

  • Single values (Values that get converted directly from a string).
  • Collections of values.
  • Dictionaries of values.
  • Complex type values (Instances that are constructed from multiple fields).

Single values

Any type that wants to be treated as a single atomic value for the purposes of binding, needs to implement IParsable<T> (we could consider requiring ISpanParsable<T> to potentially avoid unncessary allocations in the future).

All common runtime types are covered by this. Some examples are: string, char, bool, byte, sbyte, ushort, uint, ulong, Int128, short, int, long, UInt128, Half, float, double, decimal, DateOnly, DateTime, DateTimeOffset, TimeSpan, TimeOnly, Guid

Any type that is not considered directly parsable from a single value, is considered either a collection, dictionary, or object.

Collections

We support binding to a set of well-known collection types, interfaces and arrays, as well as any concrete type that implements ICollection. The set of well-known types and interfaces is:
Queue<T>, Stack<T>, ReadOnlyCollection<T>, ConcurrentBag<T>, ConcurrentStack<T>, ConcurrentQueue<T>, ImmutableArray<T>, ImmutableList<T>, ImmutableHashSet<T>, ImmutableSortedSet<T>, ImmutableQueue<T>, mmutableStack<T>, IImmutableSet<T>, IImmutableList<T>, IImmutableQueue<T>, ImmutableStack<T>, IReadOnlySet<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, Set<T>, IList<T>, ICollection<T>, IEnumerable<T>

All other concrete types in System.Collection.Generics like List<T>, LinkedList<T>, fall in the general category of types that implement ICollection, so we don't treat them differently to custom types that implement ICollection<T>.

Collection types are bound starting from 0 to the last element, and there can't be any gaps in the sequence. We simply stop binding values the moment we don't find an index. We do this for two reasons:

  • It allows us to know when to stop (we can start at 0 and stop the moment we don't find a value for a given index).
  • It prevents potential hostile situations where we might receive a large index value and inadvertedly cause a large allocation to happen.

Values for collections are specified using the square brackets syntax, so [0], [1], etc. and compose with other elements (for example in complex objects as [0]OrderId [0]Total and so on).

Dictionaries

We support binding to a set of well-known dictionary types, interfaces and arrays, as well as any concrete type that implements IDictionary<TKey, TValue>. The set of well-known types and interfaces is:
IImmutableDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>, ImmutableSortedDictionary<TKey, TValue>, ConcurrentDictionary<TKey, TValue>, SortedList<TKey, TValue>, SortedDictionary<TKey, TValue>, Dictionary<TKey, TValue>, ReadOnlyDictionary<TKey, TValue>.

All other concrete types, fall in the general category of types that implement IDictionary<TKey, TValue> for which we just create the concrete type.

If a type implements IDictionary<TKey, TValue> and ICollection<T> we treat is as a dictionary, not a collection.

When binding dictionary types, we don't know the set of keys ahead of time without iterating over all the fields in the form. This means that we can't use the same approach we use for collections, so we delay binding dictionaries as much as possible, as it forces us to compute the set of keys for the dictionary to be able to bind the values.

We do this by iterating over the set of keys and constructing a tree where each node has a key value, and each branch is tagged with the prefix for that key.

Dictionary types are bound using the square brackets syntax, so [key], [key1], [key2], etc. and compose with other elements (for example in complex objects as [key]OrderId [key]Total and so on).

Complex types

Our approach for binding complext objects is to iterate over all the properties in the object, append the property name to the value we are trying to bind as a prefix, and then recursively try to bind the value. For example, if we are trying to bind a value to Customer.Address.Street, we start trying to bind Customer without any prefix, we append Address to the current prefix we are looking for, and try to bind Address to Customer.Address, and then we append Street to the current prefix we are looking for, and try to bind Street to Customer.Address.Street.

We only create an instance for a type if we find at least one value for it. For example, we won't create an instance of Address unless we find a value for either street, city, country, etc.

When we have at least one value, we create a new instance using the parameterless constructor, and we set the values for the properties we found.

We support binding to any type that has a public parameterless constructor and public writable properties. We don't support binding to fields, or properties with private setters.

There are some features supported in other serializers that we won't support. In general, this aligns with with the support provided by Razor Pages and MVC. The set of features that we do not support are:

  • Untyped binding (binding to object directly).
  • Polymorphic binding (binding to an interface or base type but constructing a derived instance).
  • Capturing additional unstructured data (binding to a dictionary and capturing all the fields that didn't match any property).

There are other set of features that we should support but that we haven't set on a strategy. These are:

  • Ignored properties.
  • Changing the property name.
  • Required properties.
  • Custom converters for a given property.

There are several approaches we can take here:

  • Introduce a new set of public attributes.
  • Reuse the existing attributes from System.Runtime.Serialization (datacontract, datamember, etc.).
  • Use some attributes from System.ComponentModel.DataAnnotations.

In general, while these are convenient features, there is always the option to create a DTO whose shape matches exactly the fields that you are expecting to receive from the form, and then map the DTO to the actual model.

The most risk averse approach is to support [DataContract] and [DataMember] attributes, and then optionally introduce new attributes if we need them.

We could also consider supporting customization only via source generation. That "frees" us from having to take concrete dependencies in the framework and would allow us to respect attributes that we would not consider due to layering, like [BindNever] or [BindRequired] from MVC.

Extensibility

Right now we haven't built any extensibility mechanism into the system, but we can think that such a similar mechanism like the one MVC and Razor Pages has or System.Text.Json is needed. Particularly the ability to specify a custom binder for a given type, or property within a type, as well as the ability to create a factory that can handle a variable number of types. The one thing that we won't support is the ability to customize the binding process on a per request basis. That adds an unnecessary level of complexity to the pipeline and precludes us from performing critical optimizations.

General binding algorithm

The general algorithm for binding is as follows:

  • We start with an empty prefix.
  • We determine the type we are trying to bind to.
  • For simple values, we bind to the value field.
  • For collections, we start binding from 0 until we don't find a value for a given index. On each element, we recursively try to bind the value for that element.
  • For dictionaries, we compute the set of keys by parsing the keys on the form, and then iterate over the keys, and then recursively try to bind the value for each key.
  • For complex types, we iterate over all the properties in the type, and recursively try to bind the value for each property.

In general, we expect that the forms that are sent from the browser contain values for the properties that we are trying to bind. We don't try to compute the set of known prefixes ahead of time. That requires us to process the form keys and store the result of that process. Instead, we do that processing inline, by iterating over the keys and computing the set of prefixes as we go.

This means for example, that for an object, like a Customer with an Address, we only perform a lookup for a fixed set of values on the form, like Customer.Name, Customer.Address.Street, Customer.Address.City, etc. independent of how many fields were sent as part of the form.

For collections is similar, because even if there are multiple elements, we can precompute the indexes, and we visit them in order.

The only case where we are forced to inspect the entire set of form values is when dealing with dictionaries, but that's a much rarer case than the other two.

Implementation details

Right now we are using the form collection wrapped in an IReadOnlyDictonary<string,StringValues> to represent the form values. We should consider using a custom type that allows us to avoid the allocations for the keys and values.

There are a few optimizations that we can apply when we are processing the form data stream if we are able to process the underlying form data stream directly. These optimizations present variable levels of complexity:

  • We can avoid allocating the dictionary keys by reusing a set of precomputed keys (if we know the type we are binding to when we are trying to read the form and the keys can be precomputed (which except for dictionaries and collections, its usually the case)).
  • We can avoid allocating strings altogether and just use the underlying buffers from the form data stream by using a custom type that implements IReadOnlyDictionary<ReadOnlyMemory<char>, IEnumerable<ReadOnlyMemory<char>>> (or something similar).

The binding process itself doesn't allocate more than what is strictly necessary, as the only allocations that we do are for the instances that we create for the types that we are binding to. The entire binding process is typed, so value types are never boxed.

@SteveSandersonMS
Copy link
Member

There is only one property in the entire component hierarchy that gets to bind data using [SupplyParameterFromForm] and that property should be associated with the form that is handling the post request as described above

Does this mean people could never do something like the following:

@code {
    [SuplyParameterFromForm] public string UserName { get; set; }
    [SuplyParameterFromForm] public string Password { get; set; }
}

i.e., does the design imply that people must always create a single DTO representing the shape of the incoming form post?

I'm not sure whether this point in the design only refers to one scenario out of many, or whether it's saying this is literally the only supported scenario.

@javiercn
Copy link
Member Author

javiercn commented Jun 5, 2023

@SteveSandersonMS Yep, only one parameter gets to bind the form. Minimal I believe follows the same approach. This is different from MVC that would bind multiple separate values from the form.

@SteveSandersonMS
Copy link
Member

OK. How close are we to being able to do some app building to explore all of this in practical scenarios? I'm aware of some pending PRs related to model binding particular types/collections/etc (e.g., #48559). Are they what's needed for us to try it out, or are there still other parts of the internals like handling POST requests that are unrelated to an <EditForm> and so on?

@javiercn
Copy link
Member Author

javiercn commented Jun 5, 2023

How close are we to being able to do some app building to explore all of this in practical scenarios?

The two remaining missing parts are validation/error handling and antiforgery, once the bindings PRs get through.

Are they what's needed for us to try it out, or are there still other parts of the internals like handling POST requests that are unrelated to an and so on?

That's tracked by #47804

@davidfowl
Copy link
Member

@SteveSandersonMS Yep, only one parameter gets to bind the form. Minimal I believe follows the same approach. This is different from MVC that would bind multiple separate values from the form.

It does not. Minimal APIs form binding allows binding multiple parameters in .NET 8 (simple values though).

@javiercn
Copy link
Member Author

javiercn commented Jun 5, 2023

It does not. Minimal APIs form binding allows binding multiple parameters in .NET 8 (simple values though).

I thought for complex values only one thing reads the form (like JSON)

@davidfowl
Copy link
Member

Yes, for complex values, you only get one (like JSON).

@javiercn
Copy link
Member Author

javiercn commented Jun 5, 2023

@davidfowl ok, so we will do the same? (Complex values you get one, simple values, you get any you want)

@SteveSandersonMS
Copy link
Member

Yes, for complex values, you only get one (like JSON).
ok, so we will do the same? (Complex values you get one, simple values, you get any you want)

Is there some reason why that restriction is desirable? Wouldn't it be preferable for people just to be able to bind as many complex objects as they want? For example if a complex form is split up over many components. I know people could make a DTO representing the whole top-level thing, but what makes us want to force them to do that?

@javiercn
Copy link
Member Author

javiercn commented Jun 5, 2023

For example, if a complex form is split up over many components. I know people could make a DTO representing the whole top-level thing, but what makes us want to force them to do that?

How will that look like?

Is there some reason why that restriction is desirable? Wouldn't it be preferable for people just to be able to bind as many complex objects as they want? For example if a complex form is split up over many components. I know people could make a DTO representing the whole top-level thing, but what makes us want to force them to do that?

It's a scoping decision. We can always add binding multiple complex values later, but we don't have to worry about modeling all those scenarios for 8.0

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Jun 13, 2023

@javiercn I think this issue might have reached the end of its useful life since it now describes a huge amount of stuff and many different iterations of the design. Much of it is covered by other issues now (e.g., antiforgery, error handling). The issue title is also very generic and overlaps with lots of other issues, so to bring clarity, it would be good to close this.

Would you be OK with me closing this and creating a new issue for "Blazor form post model binding" and copying in the list of things from this issue description that appear to refer to model binding?

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Jun 13, 2023

Actually since I'm tidying up now I'm just going to go ahead and close this in favour of #48759. If that's the wrong thing to do please let me know.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-full-stack-web-ui Full stack web UI with Blazor
Projects
None yet
Development

No branches or pull requests

6 participants