Blazor Components and Parameters (with Examples) in .NET 10

Say you run the web store for a Portland outdoor gear shop. The product page repeats the same card markup for six items, and every price fix means editing six copies.

Blazor components fix that: you write the card once as a .razor file, pass each product in with [Parameter] properties, and send clicks back to the page with EventCallback. We’ll build that store on .NET 10, one component at a time.

Here’s what we’re building:

Blazor components demo: the finished Cascade Trail Outfitters shop on localhost:5791 with six ProductCard components, free shipping badges and an order panel showing Subtotal: $477.94 (4 items)
The finished shop after three real clicks in headless Edge
  • ProductCard: one card per product, with a quantity stepper and an Add to cart button
  • Panel: a titled box that wraps whatever markup you put inside it
  • OrderList: the cart lines and subtotal, with its C# in a code-behind file

Before You Start Building Blazor Components

You need the .NET 10 SDK (I used 10.0.402) and an editor; I use VS Code. If dotnet --version doesn’t print 10.x, check which .NET versions you have first.

If you’ve never run a Blazor app, my Blazor tutorial for beginners covers pages and routing. This post stays on components. The sample data is six products, all priced in USD:

IdProductCategoryPrice
1Trail Daypack 24LPacks$64.99
2Insulated Water BottleHydration$24.50
3Two-Person TentCamping$189.00
4Headlamp 400Lighting$34.95
5Rain Shell JacketApparel$119.00
6Camp StoveCooking$79.99

What Is a Blazor Component?

A Blazor component is a .razor file that mixes HTML markup with a C# @code block. The compiler turns each file into a C# partial class that inherits ComponentBase, named after the file. That’s plain C# inheritance at work.

PieceWhat it does
ProductCard.razorFile name = class name. It must start with an uppercase letter
@code { ... }Fields, methods and parameters of the class
[Parameter]A public property the parent sets as an attribute
@page "/shop"Turns a component into a routable page
<ProductCard ... />Uses the component inside another one

A page is just a component with @page. Everything below works the same in both.

Step 1: Create a Blazor Web App in .NET 10

Create the app from the Blazor Web App template. Its default interactivity is Server, applied per page:

dotnet new blazor -o BlazorComponents
cd BlazorComponents

I deleted the two appsettings files (this demo doesn’t use them) and trimmed Properties/launchSettings.json to one http profile on port 5791:

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "applicationUrl": "http://localhost:5791",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

The project file and Program.cs are what the template wrote for .NET 10. Program.cs already registers the Interactive Server services we’ll need in Step 4:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
  </PropertyGroup>

</Project>
using BlazorComponents.Components;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();

app.UseAntiforgery();

app.MapStaticAssets();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

New components go in the Components folder. The template’s _Imports.razor already has @using BlazorComponents.Components, so pages can use them without extra lines.

Blazor Web App template home page running on localhost:5791 with Hello, world! and the Home, Counter and Weather links
The untouched template on http://localhost:5791

Checkpoint: dotnet run prints Now listening on: http://localhost:5791, and the browser shows “Hello, world!”.

Step 2: Build Your First Blazor Component with [Parameter]

First, the data types in Models/ShopModels.cs. They’re C# records, so each one is a single line plus the cart total:

namespace BlazorComponents.Models;

public record Product(int Id, string Name, string Category, decimal Price);

public record CartLine(Product Product, int Quantity)
{
    public decimal Total => Product.Price * Quantity;
}

public record StoreInfo(string Name, string City, decimal FreeShippingOver);

Now the component, Components/ProductCard.razor. The Product property is the [Parameter] the parent fills in:


@using System.Globalization
@using BlazorComponents.Models

<div class="card mb-2">
    <div class="card-body py-2">
        <h5 class="card-title mb-1">@Product.Name</h5>
        <p class="mb-0">@Product.Category · @Product.Price.ToString("C", Us)</p>
    </div>
</div>

@code {
    private static readonly CultureInfo Us = new("en-US");

    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;
}

The "C" format with an en-US culture always prints dollars, even on a PC set to another region (more in formatting numbers in C#).

Then the page, Components/Pages/Shop.razor. It loops over the products and passes each one as an attribute:


@page "/shop"
@using BlazorComponents.Models

<PageTitle>Shop</PageTitle>

<h1>Cascade Trail Outfitters</h1>

<div class="row">
    <div class="col-md-7">
        @foreach (var product in products)
        {
            <ProductCard Product="product" />
        }
    </div>
</div>

@code {
    private List<Product> products =
    [
        new(1, "Trail Daypack 24L", "Packs", 64.99m),
        new(2, "Insulated Water Bottle", "Hydration", 24.50m),
        new(3, "Two-Person Tent", "Camping", 189.00m),
        new(4, "Headlamp 400", "Lighting", 34.95m),
        new(5, "Rain Shell Jacket", "Apparel", 119.00m),
        new(6, "Camp Stove", "Cooking", 79.99m),
    ];
}
Blazor component parameter demo: six ProductCard components on localhost:5791/shop, from Trail Daypack 24L at $64.99 to Camp Stove at $79.99
One ProductCard component, six Product parameters

Keep parameters as plain auto-properties. Microsoft Learn warns that logic in a parameter’s set can cause an infinite render loop.

Checkpoint: curl of /shop returned 200 with six cards, starting with “Packs · $64.99”.

Step 3: Make a Blazor Component Parameter Required with [EditorRequired]

[EditorRequired] tells the editor and the build that a parent must set the parameter. To see it, I added a card with no product under the loop:

<ProductCard />

dotnet build --no-incremental printed this warning:

Components\Pages\Shop.razor(14,9): warning RZ2012: Component 'ProductCard' expects a value for the parameter 'Product', but a value may not have been provided.
VS Code with Shop.razor open showing a ProductCard tag without the Product parameter and the terminal showing warning RZ2012 from dotnet build
The RZ2012 warning for a missing [EditorRequired] parameter

It’s only a warning, so the build still succeeds. At runtime, curl of /shop returned 500:

NullReferenceException: Object reference not set to an instance of an object.
at BlazorComponents.Components.ProductCard.BuildRenderTree(RenderTreeBuilder __builder) in C:\DotNetDemo\BlazorComponents\Components\ProductCard.razor:line 6

The attribute isn’t checked at runtime and doesn’t promise a non-null value, so null checks still matter for optional parameters. I removed the broken tag before moving on.

Checkpoint: one RZ2012 warning in the build output, and none once the tag is gone.

Step 4: Send Clicks from Child to Parent with EventCallback

The card has the Add button, but the cart lives in the page. A child can’t reach into its parent, so it exposes an EventCallback<CartLine> parameter and invokes it:


@using System.Globalization
@using BlazorComponents.Models

<div class="card mb-2">
    <div class="card-body py-2">
        <h5 class="card-title mb-1">@Product.Name</h5>
        <p class="mb-2">@Product.Category · @Product.Price.ToString("C", Us)</p>
        <button class="btn btn-outline-secondary btn-sm minus" @onclick="() => quantity = Math.Max(1, quantity - 1)">-</button>
        <span class="qty mx-2">@quantity</span>
        <button class="btn btn-outline-secondary btn-sm plus" @onclick="() => quantity++">+</button>
        <button class="btn btn-primary btn-sm ms-2 add" @onclick="Add">Add to cart</button>
    </div>
</div>

@code {
    private static readonly CultureInfo Us = new("en-US");
    private int quantity = 1;

    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;

    [Parameter]
    public EventCallback<CartLine> OnAdd { get; set; }

    private async Task Add() => await OnAdd.InvokeAsync(new CartLine(Product, quantity));
}

The quantity field is the card’s own private state. Learn recommends the typed EventCallback<TValue> over plain EventCallback, because a wrong argument type then fails at compile time.

In the page, hand the callback a method and give the page a render mode. Here’s the updated Shop.razor:

@page "/shop"
@rendermode InteractiveServer
@using BlazorComponents.Models

<PageTitle>Shop</PageTitle>

<h1>Cascade Trail Outfitters</h1>

<div class="row">
    <div class="col-md-7">
        @foreach (var product in products)
        {
            <ProductCard Product="product" OnAdd="AddToCart" />
        }
    </div>
    <div class="col-md-5">
        <p class="cart">Cart: @cart.Count line(s), @cart.Sum(l => l.Quantity) item(s)</p>
    </div>
</div>

@code {
    private List<Product> products =
    [
        new(1, "Trail Daypack 24L", "Packs", 64.99m),
        new(2, "Insulated Water Bottle", "Hydration", 24.50m),
        new(3, "Two-Person Tent", "Camping", 189.00m),
        new(4, "Headlamp 400", "Lighting", 34.95m),
        new(5, "Rain Shell Jacket", "Apparel", 119.00m),
        new(6, "Camp Stove", "Cooking", 79.99m),
    ];
    private readonly List<CartLine> cart = [];

    private void AddToCart(CartLine line) => cart.Add(line);
}

You don’t call StateHasChanged in AddToCart. Blazor rerenders the parent after an EventCallback runs, which is how it differs from a raw C# delegate like Action<T>.

I tried it first without the @rendermode InteractiveServer line. After three clicks, the page still said “Cart: 0 line(s), 0 item(s)”, because a page without a render mode is static HTML.

With the line back, the same clicks gave “Cart: 2 line(s), 3 item(s)”. My Blazor render modes guide explains what each mode does.

Blazor EventCallback demo: ProductCard Add to cart buttons updating the parent Shop page, which shows Cart: 2 line(s), 3 item(s)
Each Add to cart click reaches the parent through EventCallback

Checkpoint: Add on the Daypack, then + and Add on the Tent: “Cart: 2 line(s), 3 item(s)”.

Step 5: Pass Markup to a Blazor Component with ChildContent

A parameter can also carry markup. Name a RenderFragment parameter ChildContent, and whatever sits between the component’s tags lands there. Here’s Components/Panel.razor:


<div class="card">
    <div class="card-header fw-bold">@Title</div>
    <div class="card-body">
        @ChildContent
    </div>
</div>

@code {
    [Parameter]
    public string Title { get; set; } = "";

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
VS Code with Panel.razor open showing the Title parameter and the RenderFragment ChildContent parameter, and a successful dotnet build in the terminal
Panel.razor: a Title parameter plus ChildContent

The name matters: Blazor only maps the content between the tags to a property called ChildContent. In the page, the cart text moves inside the panel:

        <Panel Title="Your order">
            <p class="cart">Cart: @cart.Count line(s), @cart.Sum(l => l.Quantity) item(s)</p>
            <p class="text-muted mb-0">Free shipping on orders over $75.</p>
        </Panel>
Blazor ChildContent RenderFragment demo: a Panel component titled Your order wrapping the cart count and a free shipping note
Panel renders its Title parameter and the ChildContent markup

Checkpoint: the right column shows “Your order” with “Cart: 2 line(s), 3 item(s)” inside it.

Step 6: Move Component Logic to a Code-Behind Partial Class

For the cart list, I split markup and C# into two files. Components/OrderList.razor holds only markup:


@if (Lines.Count == 0)
{
    <p class="text-muted mb-0">Your cart is empty.</p>
}
else
{
    <ul class="list-group mb-2">
        @foreach (var line in Lines)
        {
            <li class="list-group-item d-flex justify-content-between align-items-center">
                <span>@line.Quantity x @line.Product.Name</span>
                <span>
                    @Money(line.Total)
                    <button class="btn btn-link btn-sm remove" @onclick="() => OnRemove.InvokeAsync(line)">Remove</button>
                </span>
            </li>
        }
    </ul>
    <p class="fw-bold mb-0 subtotal">Subtotal: @Money(Subtotal) (@ItemCount items)</p>
}

And Components/OrderList.razor.cs holds the class. It’s a partial class with the same name and namespace as the .razor file:

using System.Globalization;
using BlazorComponents.Models;
using Microsoft.AspNetCore.Components;

namespace BlazorComponents.Components;

public partial class OrderList : ComponentBase
{
    private static readonly CultureInfo Us = new("en-US");

    [Parameter, EditorRequired]
    public IReadOnlyList<CartLine> Lines { get; set; } = [];

    [Parameter]
    public EventCallback<CartLine> OnRemove { get; set; }

    private decimal Subtotal => Lines.Sum(l => l.Total);
    private int ItemCount => Lines.Sum(l => l.Quantity);

    private static string Money(decimal value) => value.ToString("C", Us);
}

Two details trip people up. _Imports.razor doesn’t apply to .cs files, so the using lines are needed, and : ComponentBase is optional because the .razor half already sets it.

VS Code with OrderList.razor.cs open showing public partial class OrderList : ComponentBase with Lines and OnRemove parameters and a successful dotnet build
OrderList.razor.cs, the code-behind half of the component

The subtotal is a LINQ Sum over the lines. In the page, the panel now wraps the new component, and RemoveLine handles its callback:

        <Panel Title="Your order">
            <OrderList Lines="cart" OnRemove="RemoveLine" />
        </Panel>

@code {
    ...
    private void RemoveLine(CartLine line) => cart.Remove(line);
}
Blazor OrderList component built with a code-behind partial class showing three cart lines and Subtotal: $477.94 (4 items)
The OrderList component inside the Panel

Checkpoint: after adding the Headlamp too: “Subtotal: $477.94 (4 items)”. Remove on the Tent line left “Subtotal: $99.94 (2 items)”.

Step 7: Share Values with a Blazor Cascading Parameter

Every card needs the store’s free shipping threshold, and passing it to each card by hand gets old. A CascadingValue hands a value to every component below it. The loop in Shop.razor becomes:

        <CascadingValue Value="store" IsFixed="true">
            @foreach (var product in products)
            {
                <ProductCard Product="product" OnAdd="AddToCart" />
            }
        </CascadingValue>

The page also gets a store field: new("Cascade Trail Outfitters", "Portland, OR", 75m). IsFixed="true" says it won’t change, so Blazor skips change tracking.

The card picks it up with [CascadingParameter], matched by type. Here’s the finished ProductCard.razor:


@using System.Globalization
@using BlazorComponents.Models

<div class="card mb-2">
    <div class="card-body py-2">
        <h5 class="card-title mb-1">@Product.Name</h5>
        <p class="mb-2">@Product.Category · @Product.Price.ToString("C", Us)</p>
        @if (Store is not null && Product.Price >= Store.FreeShippingOver)
        {
            <p class="mb-2"><span class="badge text-bg-success">Free shipping from @Store.City</span></p>
        }
        <button class="btn btn-outline-secondary btn-sm minus" @onclick="() => quantity = Math.Max(1, quantity - 1)">-</button>
        <span class="qty mx-2">@quantity</span>
        <button class="btn btn-outline-secondary btn-sm plus" @onclick="() => quantity++">+</button>
        <button class="btn btn-primary btn-sm ms-2 add" @onclick="Add">Add to cart</button>
    </div>
</div>

@code {
    private static readonly CultureInfo Us = new("en-US");
    private int quantity = 1;

    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;

    [Parameter]
    public EventCallback<CartLine> OnAdd { get; set; }

    [CascadingParameter]
    private StoreInfo? Store { get; set; }

    private async Task Add() => await OnAdd.InvokeAsync(new CartLine(Product, quantity));
}
Blazor cascading parameter demo: free shipping from Portland, OR badges on Two-Person Tent, Rain Shell Jacket, Camp Stove
The cascaded StoreInfo drives the free shipping badge

For an app-wide value, register it as a service with builder.Services.AddCascadingValue(...), the same container as in DI lifetimes in ASP.NET Core.

Important: cascading values don’t cross render mode boundaries. A CascadingValue in a static layout won’t reach an interactive page, while a root-level AddCascadingValue will.

Checkpoint: badges on 3 cards, the ones at $75 or more: Two-Person Tent, Rain Shell Jacket, Camp Stove.

Step 8: Keep Component State in Lists with @key

Last, a Sort by price button. Here’s the final Shop.razor; note @key="product.Id" on the card:

@page "/shop"
@rendermode InteractiveServer
@using BlazorComponents.Models

<PageTitle>Shop</PageTitle>

<h1>Cascade Trail Outfitters</h1>

<button class="btn btn-secondary btn-sm mb-3 sort" @onclick="SortByPrice">Sort by price</button>

<div class="row">
    <div class="col-md-7">
        <CascadingValue Value="store" IsFixed="true">
            @foreach (var product in products)
            {
                <ProductCard @key="product.Id" Product="product" OnAdd="AddToCart" />
            }
        </CascadingValue>
    </div>
    <div class="col-md-5">
        <Panel Title="Your order">
            <OrderList Lines="cart" OnRemove="RemoveLine" />
        </Panel>
    </div>
</div>

@code {
    private readonly StoreInfo store = new("Cascade Trail Outfitters", "Portland, OR", 75m);
    private List<Product> products =
    [
        new(1, "Trail Daypack 24L", "Packs", 64.99m),
        new(2, "Insulated Water Bottle", "Hydration", 24.50m),
        new(3, "Two-Person Tent", "Camping", 189.00m),
        new(4, "Headlamp 400", "Lighting", 34.95m),
        new(5, "Rain Shell Jacket", "Apparel", 119.00m),
        new(6, "Camp Stove", "Cooking", 79.99m),
    ];
    private readonly List<CartLine> cart = [];

    private void AddToCart(CartLine line) => cart.Add(line);
    private void RemoveLine(CartLine line) => cart.Remove(line);
    private void SortByPrice() => products = [.. products.OrderBy(p => p.Price)];
}

I ran the test without @key first: + twice on the Daypack, so it showed 3, then Sort. Row 1 became “Insulated Water Bottle qty 3”, and the Daypack in row 3 dropped back to 1.

Without a key, Blazor reuses the card in row 1 and only swaps its Product parameter, so the private quantity stays behind. With @key, the same clicks left “Trail Daypack 24L qty 3” in row 3.

Blazor @key before and after: without @key the quantity 3 stays on the first row (Insulated Water Bottle), with @key the quantity 3 moves with the Trail Daypack 24L to row three
Same clicks, same sort: without @key vs with @key

SortByPrice builds a new List rather than sorting in place, which is fine. Use a unique, stable key such as a database id or the object itself.

Duplicate keys under one parent throw an exception, and keys only compare among siblings.

Checkpoint: after sorting with @key, the Daypack sits third and still shows 3.

Test the Blazor Components

Here’s every check I ran on the finished app, with the real results:

CheckHowResult
/shop loadscurl200
Missing [EditorRequired] valuedotnet buildwarning RZ2012
Add to cart, no render mode3 clicks, headless EdgeCart: 0 line(s), 0 item(s)
Add to cart, InteractiveServer3 clicks, headless EdgeCart: 2 line(s), 3 item(s)
Final cart4 clicks, headless EdgeSubtotal: $477.94 (4 items)
Sort without @keyqty 3, then SortInsulated Water Bottle qty 3
Sort with @keyqty 3, then SortTrail Daypack 24L qty 3

The page is still prerendered: curl got full HTML plus a <!--Blazor: marker. That’s why my headless runs wait 3.5 seconds before clicking, so the circuit is connected.

Troubleshooting Common Blazor Component Parameter Mistakes

Writing to a parameter inside the child. This QtyBadge.razor increments its own Quantity parameter:


@using BlazorComponents.Models

<p class="mb-2">
    @Product.Name: <span class="qty">@Quantity</span>
    <button class="btn btn-outline-secondary btn-sm plus" @onclick="() => Quantity++">+</button>
</p>

@code {
    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;

    [Parameter]
    public int Quantity { get; set; }   // the child writes to its own parameter: don't
}

I used it on a test page, Components/Pages/Mistake.razor, with a button that only rerenders the parent:


@page "/mistake"
@rendermode InteractiveServer
@using BlazorComponents.Models

<PageTitle>Mistake</PageTitle>

<h1>Writing to a parameter</h1>

<QtyBadge Product="headlamp" Quantity="1" />

<p>Parent renders: <span class="renders">@renders</span></p>
<button class="btn btn-secondary btn-sm refresh" @onclick="() => renders++">Re-render parent</button>

@code {
    private readonly Product headlamp = new(4, "Headlamp 400", "Lighting", 34.95m);
    private int renders = 1;
}

Two clicks on + showed 3. One click on Re-render parent reset it to 1, because the parent sent Quantity="1" again. Keep the value in a private field, as ProductCard does, and report changes with an EventCallback, Blazor’s take on C# events.

Buttons that do nothing. The page has no @rendermode, so it’s static SSR. Add @rendermode InteractiveServer to the page, as in Step 4.

A component tag that renders as plain HTML. The file name starts with a lowercase letter, or its namespace isn’t imported. Rename it to ProductCard.razor and check _Imports.razor.

required or init on a parameter. Learn says not to use either. Blazor sets parameters by reflection, which bypasses both, so use [EditorRequired] instead.

Where to Take It Next with Blazor Components

Swap the hard-coded list for real data and a grid. My Blazor QuickGrid tutorial sorts and pages 200 orders with the same kind of component parameters.

You can also make ProductCard generic with @typeparam, or add two-way binding with a Value parameter plus a ValueChanged callback. New posts land in the Blazor tutorials.

A few more reads for the C# behind these components:

Frequently Asked Questions

What are Blazor components?

They’re reusable pieces of UI written as .razor files that mix HTML and C#. Each file compiles to a C# class that inherits ComponentBase, and a component becomes a page when you add an @page directive.

How do I pass parameters to a Blazor component?

Declare a public auto-property with the [Parameter] attribute in the child, then set it as an attribute where you use the component, for example . Add [EditorRequired] if the parent must always set it.

How does a child component pass data to its parent in Blazor?

Give the child an EventCallback parameter and call await OnAdd.InvokeAsync(value) from its event handler. The parent assigns a method to it, and Blazor rerenders the parent after the method runs.

What is the difference between a component parameter and a cascading parameter?

A component parameter is set by the direct parent as an attribute. A cascading parameter is supplied by a CascadingValue component, or AddCascadingValue in Program.cs, and reaches every component below it, matched by type or name.

What is ComponentBase in Blazor?

ComponentBase is the base class every .razor component inherits. It provides the lifecycle methods such as OnInitialized and OnParametersSet, StateHasChanged and the rendering logic. A code-behind partial class can inherit it explicitly.

Why doesn’t my Blazor button do anything?

The page renders with static SSR, which is the default in a Blazor Web App. Add @rendermode InteractiveServer to the page or turn on global interactivity. See ASP.NET Core Razor components on Microsoft Learn.